WAVEFORM
Overview
The WAVEFORM function fits periodic waveform models to experimental data using non-linear least squares regression. It provides a selection of predefined waveform models commonly encountered in signal processing, physics, and engineering applications, including sinusoidal waves, sawtooth waves, and square waves with configurable parameters.
This function leverages scipy.optimize.curve_fit from the SciPy library to minimize the sum of squared residuals between the observed data and the model function. The underlying algorithm uses the Levenberg-Marquardt method for unconstrained problems or the Trust Region Reflective (TRF) method when parameter bounds are specified. For more details on the optimization methods, see the SciPy least_squares documentation.
The function supports six waveform models:
- Sine Wave Basic: A standard sine wave with adjustable center, width, amplitude, and offset: y = y_0 + A \sin\left(\frac{\pi(x - x_c)}{w}\right)
- Sine Wave Squared: A squared sine wave useful for modeling intensity or power: y = y_0 + A \sin^2\left(\frac{\pi(x - x_c)}{w}\right)
- Sine Wave Exponentially Damped: A decaying sinusoidal oscillation common in damped harmonic systems: y = y_0 + A e^{-x/t_0} \sin\left(\frac{\pi(x - x_c)}{w}\right)
- Sawtooth Wave Periodic: A linear ramp that resets periodically: y = y_0 + \frac{A}{T} \cdot \text{mod}(x - x_0, T)
- Square Wave Symmetric: A symmetric two-level periodic waveform with 50% duty cycle
- Square Wave Variable Duty Cycle: A square wave with adjustable duty cycle parameter
The function returns the fitted parameter values along with their standard errors, derived from the covariance matrix of the fit. These errors are computed as the square root of the diagonal elements of the covariance matrix, providing estimates of parameter uncertainty based on a linear approximation around the optimal solution.
This example function is provided as-is without any representation of accuracy.
Excel Usage
=WAVEFORM(xdata, ydata, waveform_model)
xdata(list[list], required): The xdata valueydata(list[list], required): The ydata valuewaveform_model(str, required): The waveform_model value
Returns (list[list]): 2D list [param_names, fitted_values, std_errors], or error string.
Examples
Example 1: Demo case 1
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| sawtooth_wave_periodic | 0.1 | 0.07788526495582983 |
| 1.3250000000000002 | 0.7150127784838747 | |
| 2.5500000000000003 | 1.4191650856475282 | |
| 3.7750000000000004 | 2.1275338136813193 | |
| 5 | 2.7117273507825037 |
Excel formula:
=WAVEFORM("sawtooth_wave_periodic", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {0.07788526495582983;0.7150127784838747;1.4191650856475282;2.1275338136813193;2.7117273507825037})
Expected output:
"non-error"
Example 2: Demo case 2
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| sine_wave_basic | 0.1 | 2.7374148538022562 |
| 1.3250000000000002 | -2.5091778071395767 | |
| 2.5500000000000003 | 1.9496796048687413 | |
| 3.7750000000000004 | -0.6889523065774433 | |
| 5 | -0.7708681386612977 |
Excel formula:
=WAVEFORM("sine_wave_basic", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.7374148538022562;-2.5091778071395767;1.9496796048687413;-0.6889523065774433;-0.7708681386612977})
Expected output:
"non-error"
Example 3: Demo case 3
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| sine_wave_exponentially_damped | 0.01 | 2.4433955885226224 |
| 2.0075 | 1.3428893089222496 | |
| 4.005 | 0.5938760148936298 | |
| 6.0024999999999995 | 0.09477767694683044 | |
| 8 | -0.06536814584538272 |
Excel formula:
=WAVEFORM("sine_wave_exponentially_damped", {0.01;2.0075;4.005;6.0024999999999995;8}, {2.4433955885226224;1.3428893089222496;0.5938760148936298;0.09477767694683044;-0.06536814584538272})
Expected output:
"non-error"
Example 4: Demo case 4
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| sine_wave_squared | 0.1 | 2.6449440834552127 |
| 1.3250000000000002 | 2.5765705507180185 | |
| 2.5500000000000003 | 1.3159662905150942 | |
| 3.7750000000000004 | 0.09365364702908215 | |
| 5 | 0.2608404481371305 |
Excel formula:
=WAVEFORM("sine_wave_squared", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.6449440834552127;2.5765705507180185;1.3159662905150942;0.09365364702908215;0.2608404481371305})
Expected output:
"non-error"
Example 5: Demo case 5
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| square_wave_symmetric | 0.1 | 2.770683835789236 |
| 1.3250000000000002 | 2.744242491212298 | |
| 2.5500000000000003 | 1.07697060932013 | |
| 3.7750000000000004 | 1.1134209821908079 | |
| 5 | 1.0402495430764112 |
Excel formula:
=WAVEFORM("square_wave_symmetric", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.770683835789236;2.744242491212298;1.07697060932013;1.1134209821908079;1.0402495430764112})
Expected output:
"non-error"
Example 6: Demo case 6
Inputs:
| waveform_model | xdata | ydata |
|---|---|---|
| square_wave_variable_duty_cycle | 0.1 | 2.770683835789236 |
| 1.3250000000000002 | 2.744242491212298 | |
| 2.5500000000000003 | 2.77697060932013 | |
| 3.7750000000000004 | 1.1134209821908079 | |
| 5 | 1.0402495430764112 |
Excel formula:
=WAVEFORM("square_wave_variable_duty_cycle", {0.1;1.3250000000000002;2.5500000000000003;3.7750000000000004;5}, {2.770683835789236;2.744242491212298;2.77697060932013;1.1134209821908079;1.0402495430764112})
Expected output:
"non-error"
Python Code
import numpy as np
from scipy.optimize import curve_fit as scipy_curve_fit
import math
def waveform(xdata, ydata, waveform_model):
"""
Fits waveform models to data using scipy.optimize.curve_fit. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html for details.
See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
This example function is provided as-is without any representation of accuracy.
Args:
xdata (list[list]): The xdata value
ydata (list[list]): The ydata value
waveform_model (str): The waveform_model value Valid options: Sawtooth Wave Periodic, Sine Wave Basic, Sine Wave Exponentially Damped, Sine Wave Squared, Square Wave Symmetric, Square Wave Variable Duty Cycle.
Returns:
list[list]: 2D list [param_names, fitted_values, std_errors], or error string.
"""
def _validate_data(xdata, ydata):
"""Validate and convert both xdata and ydata to numpy arrays."""
for name, arg in [("xdata", xdata), ("ydata", ydata)]:
if not isinstance(arg, list) or len(arg) < 2:
raise ValueError(f"{name}: must be a 2D list with at least two rows")
vals = []
for i, row in enumerate(arg):
if not isinstance(row, list) or len(row) == 0:
raise ValueError(f"{name} row {i}: must be a non-empty list")
try:
vals.append(float(row[0]))
except Exception:
raise ValueError(f"{name} row {i}: non-numeric value")
if name == "xdata":
x_arr = np.asarray(vals, dtype=np.float64)
else:
y_arr = np.asarray(vals, dtype=np.float64)
if x_arr.shape[0] != y_arr.shape[0]:
raise ValueError("xdata and ydata must have the same number of rows")
return x_arr, y_arr
# Model definitions dictionary
models = {
'sawtooth_wave_periodic': {
'params': ['x0', 'y0', 'A', 'T'],
'model': lambda x, x0, y0, A, T: y0 + (A / T) * np.mod(x - x0, T),
'guess': lambda xa, ya: (float(np.min(xa)), float(np.mean(ya)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(np.ptp(xa) if np.ptp(xa) else 1.0)),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'sine_wave_basic': {
'params': ['xc', 'w', 'A', 'y0'],
'model': lambda x, xc, w, A, y0: y0 + A * np.sin(np.pi * (x - xc) / w),
'guess': lambda xa, ya: (float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-6)), float(np.ptp(ya) / 2.0 if np.ptp(ya) else 1.0), float(np.mean(ya))),
'bounds': ([-np.inf, 0.0, -np.inf, -np.inf], np.inf),
},
'sine_wave_exponentially_damped': {
'params': ['y0', 'xc', 'w', 't0', 'A'],
'model': lambda x, y0, xc, w, t0, A: y0 + A * np.exp(-x / t0) * np.sin(np.pi * (x - xc) / w),
'guess': lambda xa, ya: (float(np.mean(ya)), float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), 1.0, float(np.ptp(ya) if np.ptp(ya) else 1.0)),
'bounds': ([-np.inf, -np.inf, 0.0, 0.0, -np.inf], np.inf),
},
'sine_wave_squared': {
'params': ['xc', 'w', 'A', 'y0'],
'model': lambda x, xc, w, A, y0: y0 + A * np.power(np.sin(np.pi * (x - xc) / w), 2),
'guess': lambda xa, ya: (float(np.median(xa)), float(max((np.max(xa) - np.min(xa)) / 4.0, 1e-3)), float(np.ptp(ya) if np.ptp(ya) else 1.0), float(np.mean(ya))),
'bounds': ([-np.inf, 0.0, -np.inf, -np.inf], np.inf),
},
'square_wave_symmetric': {
'params': ['a', 'b', 'x0', 'T'],
'model': lambda x, a, b, x0, T: np.where(np.mod(x - x0, T) < 0.5 * T, a, b),
'guess': lambda xa, ya: (float(np.max(ya)), float(np.min(ya)), float(np.min(xa)), float(np.ptp(xa) if np.ptp(xa) else 1.0)),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0], np.inf),
},
'square_wave_variable_duty_cycle': {
'params': ['a', 'b', 'x0', 'duty', 'T'],
'model': lambda x, a, b, x0, duty, T: np.where(np.mod(x - x0, T) < duty * T, a, b),
'guess': lambda xa, ya: (float(np.max(ya)), float(np.min(ya)), float(np.min(xa)), 0.5, float(np.ptp(xa) if np.ptp(xa) else 1.0)),
'bounds': ([-np.inf, -np.inf, -np.inf, 0.0, 0.0], [np.inf, np.inf, np.inf, 1.0, np.inf]),
}
}
# Validate model parameter
if waveform_model not in models:
return f"Invalid model: {str(waveform_model)}. Valid models are: {', '.join(models.keys())}"
model_info = models[waveform_model]
# Validate and convert input data
try:
x_arr, y_arr = _validate_data(xdata, ydata)
except ValueError as e:
return f"Invalid input: {e}"
# Perform curve fitting
try:
p0 = model_info['guess'](x_arr, y_arr)
bounds = model_info.get('bounds', (-np.inf, np.inf))
if bounds == (-np.inf, np.inf):
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, maxfev=10000)
else:
popt, pcov = scipy_curve_fit(model_info['model'], x_arr, y_arr, p0=p0, bounds=bounds, maxfev=10000)
fitted_vals = [float(v) for v in popt]
for v in fitted_vals:
if math.isnan(v) or math.isinf(v):
return "Fitting produced invalid numeric values (NaN or inf)."
except ValueError as e:
return f"Initial guess error: {e}"
except Exception as e:
return f"curve_fit error: {e}"
# Calculate standard errors
std_errors = None
try:
if pcov is not None and np.isfinite(pcov).all():
std_errors = [float(v) for v in np.sqrt(np.diag(pcov))]
except Exception:
pass
return [model_info['params'], fitted_vals, std_errors] if std_errors else [model_info['params'], fitted_vals]