From 5ec69b1f9025421b8b559d2e5e3624acc4a853de Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Tue, 18 Nov 2025 20:43:39 +0000 Subject: [PATCH 1/5] add triangle periodic fit mode --- specparam/modes/definitions.py | 24 +++++++++++++++++++++++- specparam/modes/funcs.py | 26 ++++++++++++++++++++++++++ specparam/tests/modes/test_funcs.py | 11 ++++++++--- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/specparam/modes/definitions.py b/specparam/modes/definitions.py index 4f7c540f..b7ecd20c 100644 --- a/specparam/modes/definitions.py +++ b/specparam/modes/definitions.py @@ -6,7 +6,8 @@ from specparam.modes.mode import Mode from specparam.modes.params import ParamDefinition from specparam.modes.funcs import (expo_function, expo_nk_function, double_expo_function, - gaussian_function, skewed_gaussian_function, cauchy_function) + gaussian_function, skewed_gaussian_function, + cauchy_function, triangle_function) from specparam.modes.jacobians import jacobian_gauss from specparam.utils.checks import check_selection @@ -149,12 +150,33 @@ powers_space='log10', ) +## PE - Triangle Mode + +params_triangle = ParamDefinition(OrderedDict({ + 'cf' : 'Center frequency of the peak.', + 'pw' : 'Power of the peak, over and above the aperiodic component.', + 'bw' : 'Bandwidth of the peak.', +})) + +pe_triangle = Mode( + name='triangle', + component='periodic', + description='Triangle peak fit function.', + func=triangle_function, + jacobian=None, + params=params_triangle, + ndim=2, + freq_space='linear', + powers_space='log10', +) + # Collect available periodic modes PE_MODES = { 'gaussian' : pe_gaussian, 'skewed_gaussian' : pe_skewed_gaussian, 'cauchy' : pe_cauchy, + 'triangle' : pe_triangle, } ################################################################################################### diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py index 625a3b4a..a0b0e604 100644 --- a/specparam/modes/funcs.py +++ b/specparam/modes/funcs.py @@ -90,6 +90,32 @@ def cauchy_function(xs, *params): return ys +def triangle_function(xs, *params): + """Triangle fitting function. + + Parameters + ---------- + xs : 1d array + Input x-axis values. + *params : float + Parameters that define a cauchy function. + + Returns + ------- + ys : 1d array + Output values for triangle function. + """ + + ys = np.zeros_like(xs) + fs = xs[1] - xs[0] + + for ctr, hgt, wid in zip(*[iter(params)] * 3): + ys[np.abs(xs - ctr) <= (wid * fs)] += \ + hgt * (np.arccos(np.cos(np.linspace(0, 2 * np.pi, int(np.ceil(wid / fs)) + 1))) / np.pi) + + return ys + + ## APERIODIC FUNCTIONS def expo_function(xs, *params): diff --git a/specparam/tests/modes/test_funcs.py b/specparam/tests/modes/test_funcs.py index 069136ba..62a0415f 100644 --- a/specparam/tests/modes/test_funcs.py +++ b/specparam/tests/modes/test_funcs.py @@ -13,10 +13,8 @@ def test_gaussian_function(): ctr, hgt, wid = 50, 5, 10 - xs = np.arange(1, 100) ys = gaussian_function(xs, ctr, hgt, wid) - assert np.all(ys) # Check distribution matches generated gaussian from scipy @@ -46,12 +44,19 @@ def test_skewed_gaussian_function(): def test_cauchy_function(): ctr, hgt, wid = 50, 5, 10 - xs = np.arange(1, 100) ys = cauchy_function(xs, ctr, hgt, wid) assert np.all(ys) +def test_triangle_function(): + + ctr, hgt, wid = 50, 5, 10 + xs = np.arange(1, 100) + ys = triangle_function(xs, ctr, hgt, wid) + + assert np.all(ys) + ## Aperiodic functions def test_expo_function(): From 1f81cffa9f9a565da7f8fdec2f20e829a9589f0a Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Tue, 18 Nov 2025 22:02:40 +0000 Subject: [PATCH 2/5] minor refactor / fixes --- specparam/modes/funcs.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py index a0b0e604..817ed8dc 100644 --- a/specparam/modes/funcs.py +++ b/specparam/modes/funcs.py @@ -53,9 +53,7 @@ def skewed_gaussian_function(xs, *params): ys = np.zeros_like(xs) - for ii in range(0, len(params), 4): - - ctr, hgt, wid, skew = params[ii:ii+4] + for ctr, hgt, wid, skew in zip(*[iter(params)] * 4): ts = (xs - ctr) / wid temp = 2 / wid * (1 / np.sqrt(2 * np.pi) * np.exp(-ts**2 / 2)) * \ @@ -98,7 +96,7 @@ def triangle_function(xs, *params): xs : 1d array Input x-axis values. *params : float - Parameters that define a cauchy function. + Parameters that define a triangle function. Returns ------- @@ -110,8 +108,9 @@ def triangle_function(xs, *params): fs = xs[1] - xs[0] for ctr, hgt, wid in zip(*[iter(params)] * 3): - ys[np.abs(xs - ctr) <= (wid * fs)] += \ - hgt * (np.arccos(np.cos(np.linspace(0, 2 * np.pi, int(np.ceil(wid / fs)) + 1))) / np.pi) + + temp = np.arccos(np.cos(np.linspace(0, 2 * np.pi, int(np.ceil(wid / fs)) + 1))) + ys[np.abs(xs - ctr) <= (wid * fs)] += hgt * normalize(temp) return ys From d22efc0e9677f5ef30d22fee9b574dace6589332 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 19 Nov 2025 00:33:10 +0000 Subject: [PATCH 3/5] tweak triangle implementation --- specparam/modes/funcs.py | 6 ++++-- specparam/tests/modes/test_funcs.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py index 817ed8dc..85b02531 100644 --- a/specparam/modes/funcs.py +++ b/specparam/modes/funcs.py @@ -109,8 +109,10 @@ def triangle_function(xs, *params): for ctr, hgt, wid in zip(*[iter(params)] * 3): - temp = np.arccos(np.cos(np.linspace(0, 2 * np.pi, int(np.ceil(wid / fs)) + 1))) - ys[np.abs(xs - ctr) <= (wid * fs)] += hgt * normalize(temp) + n_samples = int(np.ceil(wid / fs)) + n_samples += 1 if n_samples % 2 == 0 else 0 + temp = np.arccos(np.cos(np.linspace(0, 2 * np.pi, n_samples))) + ys[np.abs(xs - ctr) <= (n_samples / 2) * fs] += hgt * normalize(temp) return ys diff --git a/specparam/tests/modes/test_funcs.py b/specparam/tests/modes/test_funcs.py index 62a0415f..a831bfd6 100644 --- a/specparam/tests/modes/test_funcs.py +++ b/specparam/tests/modes/test_funcs.py @@ -13,7 +13,7 @@ def test_gaussian_function(): ctr, hgt, wid = 50, 5, 10 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = gaussian_function(xs, ctr, hgt, wid) assert np.all(ys) @@ -26,7 +26,7 @@ def test_skewed_gaussian_function(): # Check that with no skew, approximate gaussian ctr, hgt, wid, skew = 50, 5, 10, 1 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys_gaus = gaussian_function(xs, ctr, hgt, wid) ys_skew = skewed_gaussian_function(xs, ctr, hgt, wid, skew) np.allclose(ys_gaus, ys_skew, atol=0.001) @@ -44,7 +44,7 @@ def test_skewed_gaussian_function(): def test_cauchy_function(): ctr, hgt, wid = 50, 5, 10 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = cauchy_function(xs, ctr, hgt, wid) assert np.all(ys) @@ -52,10 +52,10 @@ def test_cauchy_function(): def test_triangle_function(): ctr, hgt, wid = 50, 5, 10 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = triangle_function(xs, ctr, hgt, wid) - assert np.all(ys) + assert np.any(ys) ## Aperiodic functions @@ -63,7 +63,7 @@ def test_expo_function(): off, knee, exp = 10, 5, 2 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = expo_function(xs, off, knee, exp) assert np.all(ys) @@ -79,7 +79,7 @@ def test_expo_nk_function(): off, exp = 10, 2 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = expo_nk_function(xs, off, exp) assert np.all(ys) @@ -111,7 +111,7 @@ def test_linear_function(): off, sl = 10, 2 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = linear_function(xs, off, sl) assert np.all(ys) @@ -125,7 +125,7 @@ def test_quadratic_function(): off, sl, curve = 10, 3, 2 - xs = np.arange(1, 100) + xs = np.arange(1, 100, 1.) ys = quadratic_function(xs, off, sl, curve) assert np.all(ys) From 81b332198c42d36bb0dd34c3b188e5a52b7cc203 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 19 Nov 2025 10:36:13 +0000 Subject: [PATCH 4/5] update wid param in triangle --- specparam/modes/funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py index 85b02531..ac013ff8 100644 --- a/specparam/modes/funcs.py +++ b/specparam/modes/funcs.py @@ -109,7 +109,7 @@ def triangle_function(xs, *params): for ctr, hgt, wid in zip(*[iter(params)] * 3): - n_samples = int(np.ceil(wid / fs)) + n_samples = int(np.ceil(2 * wid / fs)) n_samples += 1 if n_samples % 2 == 0 else 0 temp = np.arccos(np.cos(np.linspace(0, 2 * np.pi, n_samples))) ys[np.abs(xs - ctr) <= (n_samples / 2) * fs] += hgt * normalize(temp) From af14988c651bc5d6c2405448bcf6e0c7bb20f527 Mon Sep 17 00:00:00 2001 From: Tom Donoghue Date: Wed, 19 Nov 2025 14:42:36 +0000 Subject: [PATCH 5/5] fs -> fres in triangle --- specparam/modes/funcs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py index ac013ff8..f4ba97fe 100644 --- a/specparam/modes/funcs.py +++ b/specparam/modes/funcs.py @@ -105,14 +105,14 @@ def triangle_function(xs, *params): """ ys = np.zeros_like(xs) - fs = xs[1] - xs[0] + fres = xs[1] - xs[0] for ctr, hgt, wid in zip(*[iter(params)] * 3): - n_samples = int(np.ceil(2 * wid / fs)) + n_samples = int(np.ceil(2 * wid / fres)) n_samples += 1 if n_samples % 2 == 0 else 0 temp = np.arccos(np.cos(np.linspace(0, 2 * np.pi, n_samples))) - ys[np.abs(xs - ctr) <= (n_samples / 2) * fs] += hgt * normalize(temp) + ys[np.abs(xs - ctr) <= (n_samples / 2) * fres] += hgt * normalize(temp) return ys