From 5ec69b1f9025421b8b559d2e5e3624acc4a853de Mon Sep 17 00:00:00 2001
From: Tom Donoghue
Date: Tue, 18 Nov 2025 20:43:39 +0000
Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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/6] 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
From 7b3c0d7d38f2d6a7ead15e7307ed65e1a4bc1a5d Mon Sep 17 00:00:00 2001
From: Tom Donoghue
Date: Tue, 14 Apr 2026 18:23:27 +0100
Subject: [PATCH 6/6] add formula
---
specparam/modes/definitions.py | 1 +
specparam/modes/funcs.py | 12 +++++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/specparam/modes/definitions.py b/specparam/modes/definitions.py
index dcff0a86..2384d3e2 100644
--- a/specparam/modes/definitions.py
+++ b/specparam/modes/definitions.py
@@ -168,6 +168,7 @@
name='triangle',
component='periodic',
description='Triangle peak fit function.',
+ formula=r'\text{tri}(x) = \begin{cases} 1 - |x| & \text{if } |x| < 1 \\ 0 & \text{if } |x| \geq 1 \end{cases}',
func=triangle_function,
jacobian=None,
params=params_triangle,
diff --git a/specparam/modes/funcs.py b/specparam/modes/funcs.py
index 6bc36323..14752879 100644
--- a/specparam/modes/funcs.py
+++ b/specparam/modes/funcs.py
@@ -128,7 +128,7 @@ def cauchy_function(xs, *params):
def triangle_function(xs, *params):
- """Triangle fitting function.
+ r"""Triangle fitting function.
Parameters
----------
@@ -141,6 +141,16 @@ def triangle_function(xs, *params):
-------
ys : 1d array
Output values for triangle function.
+
+ Notes
+ -----
+ Defines a triangular fit function as:
+
+ .. math::
+
+ \text{tri}(x) = \begin{cases} 1 - |x| & \text{if } |x| < 1 \\ 0 & \text{if } |x| \geq 1 \end{cases}
+
+ To use this function as a peak function, this function is scaled by hgt and wid, and moved to ctr.
"""
ys = np.zeros_like(xs)