Development environment setup#1
Conversation
Co-authored-by: Sebastian Jiro Schlecht <SebastianJiroSchlecht@users.noreply.github.com>
Implement probe(z) method on all leaf DSP modules (Gain, parallelGain, Matrix, Delay, parallelDelay, Filter, parallelFilter, SOSFilter, parallelSOSFilter, FFT, iFFT, Transform) and graph modules (Series, Parallel, Recursion, Shell). Add flamo/processor/probe.py with: - probe_points(): vectorized probe over multiple z-plane points - probe_with_derivative(): dH/dz via autograd Wirtinger reconstruction Export new helpers from flamo/processor/__init__.py. Add comprehensive test suite (30 tests) covering: - Gain/Matrix constant probe - Delay probe vs analytic formula - Filter probe vs polynomial evaluation - SOS probe vs section formula - Series/Parallel/Recursion probe vs manual matrix computation - probe_with_derivative vs finite differences (including Recursion) - Shell core-only default and include_shell_io modes All existing forward() behavior is unchanged. Co-authored-by: Sebastian Jiro Schlecht <SebastianJiroSchlecht@users.noreply.github.com>
|
Cursor Agent can help with this pull request. Just |
1. HIGH: Add allow_unused=True to torch.autograd.grad calls and handle None returns. Fixes crash when trainable Gain has grad_fn from params but probe output doesn't depend on z. 2. MEDIUM: Replace in-place tensor assignment with torch.stack to preserve autograd graph when create_graph=True. 3. MEDIUM: Add explicit NotImplementedError probe() overrides for Biquad, parallelBiquad, SVF, parallelSVF, GEQ, parallelGEQ, PEQ, parallelPEQ, AccurateGEQ, parallelAccurateGEQ, ScatteringMatrix, VelvetNoiseMatrix. Prevents silent incorrect inheritance from Filter.probe. 4. LOW: Remove unused to_complex import from system.py. Add tests: trainable Gain derivative, create_graph preservation, Biquad/SVF probe raises NotImplementedError. Co-authored-by: Sebastian Jiro Schlecht <SebastianJiroSchlecht@users.noreply.github.com>
Add probe_recursion() and probe_recursion_with_derivative() to Recursion: - probe_recursion(z, derivative=False): P(z) = I - F(z)B(z) - probe_recursion(z, derivative=True): returns (P, dP/dz) - probe_recursion_with_derivative(z, create_graph=False): returns (P, dP/dz) Factor reusable wirtinger_derivative() helper in probe.py: - Generic Wirtinger dF/dz for any callable F(z) -> Tensor - Refactor probe_with_derivative to use it internally - Export from flamo.processor Both methods accept ext_param (routed to feedforward/feedback branches) and include_shell_io/create_graph kwargs for signature compatibility. Add 16 new tests covering: - A) Characteristic value correctness (constant gains, gain+delay) - B) Derivative vs finite difference (delay, filter, constant gain) - C) API behavior (return types, shapes, kwargs, consistency) - D) Non-regression (existing probe/probe_with_derivative unchanged) All 50 tests pass. No reversed-form probing in this patch. Co-authored-by: Sebastian Jiro Schlecht <SebastianJiroSchlecht@users.noreply.github.com>
Export a scalar Wirtinger derivative helper and add w-plane probing support across DSP and system modules.
- Exported wirtinger_derivative_scalar in flamo.processor.__init__ and implemented it in probe.py to compute d/dz of complex scalar eval functions (avoids matrix inversion when computing d/dz log det).
- Added probe_w methods for Gain, parallelGain, Delay, and parallelDelay to evaluate modules at w = z^{-1> (used for numerical stability when |z|<1).
- Implemented Series.probe_w to compose module probe_w/probe calls.
- Extended Recursion with z- and w-plane helpers: corrected characteristic convention in docstrings, updated probe_recursion_with_derivative to return (P, dP/dz), added log_det_derivative (z), _eval_characteristic_w, log_det_derivative_w, and probe_recursion_w_with_derivative for w-plane evaluation and derivatives.
- Clarified Delay documentation and made probe implementations consistently compute z^{-m} vs w^{m} with explicit handling of z inverse.
These changes improve numerical stability for evaluations inside the unit circle and provide a dedicated scalar derivative helper for stable log-det derivative computations.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed:
Recursion.probedoesn't splitext_paramfor branches- Modified Recursion.probe to call _split_ext_param and pass the split parameters to feedforward and feedback branches.
- ✅ Fixed:
Parallel.probedoesn't splitext_paramfor branches- Modified Parallel.probe to split ext_param by 'branchA' and 'branchB' keys before passing to each branch.
- ✅ Fixed:
probe_wfallback toprobegives wrong z-transform- Removed incorrect fallback to probe in Series.probe_w and Recursion._eval_characteristic_w to prevent wrong z-transform computation.
- ✅ Fixed: Inherited
Gain.probecrashes forHouseholderMatrixsubclass- Added probe and probe_w overrides to HouseholderMatrix to correctly compute I - 2uu^T transfer matrix and avoid calling to_complex on already-complex tensor.
Or push these changes by commenting:
@cursor push e0aa132c68
Preview (e0aa132c68)
diff --git a/flamo/processor/dsp.py b/flamo/processor/dsp.py
--- a/flamo/processor/dsp.py
+++ b/flamo/processor/dsp.py
@@ -776,6 +776,26 @@
f"parameter shape = {self.size} not compatible with input signal of shape = ({x.shape})."
)
+ def probe(self, z: torch.Tensor, ext_param=None):
+ r"""
+ Evaluate the Householder transfer matrix H(z) at arbitrary complex z.
+
+ For a frequency-independent Householder matrix, H(z) = I - 2*u*u^T.
+
+ **Returns**:
+ torch.Tensor: ``(N, N)`` complex transfer matrix.
+ """
+ param = ext_param if ext_param is not None else self.param
+ u = self.map(param)
+ N = u.shape[0]
+ I = torch.eye(N, dtype=u.dtype, device=u.device)
+ uuT = torch.matmul(u, u.transpose(0, 1))
+ return I - 2 * uuT
+
+ def probe_w(self, w: torch.Tensor, ext_param=None):
+ r"""Evaluate at :math:`w = z^{-1}`; Householder matrix is constant, so same as :meth:`probe`."""
+ return self.probe(w, ext_param)
+
def get_io(self):
r"""
Computes the number of input and output channels based on the size parameter.
diff --git a/flamo/processor/system.py b/flamo/processor/system.py
--- a/flamo/processor/system.py
+++ b/flamo/processor/system.py
@@ -326,11 +326,11 @@
def probe_w(self, w: torch.Tensor, ext_param=None):
r"""
Evaluate the series transfer matrix at :math:`w = z^{-1}`.
- Uses each module's :meth:`probe_w` if present, else :meth:`probe(w)`.
+ Uses each module's :meth:`probe_w` if present.
"""
H = None
for module in self:
- probe_fn = getattr(module, 'probe_w', None) or getattr(module, 'probe', None)
+ probe_fn = getattr(module, 'probe_w', None)
if probe_fn is None:
continue
Hi = probe_fn(w, ext_param)
@@ -538,8 +538,9 @@
**Returns**:
torch.Tensor: Transfer matrix ``(N_out, N_in)`` (complex).
"""
- F = self.feedforward.probe(z, ext_param)
- B = self.feedback.probe(z, ext_param)
+ ext_param_ff, ext_param_fb = self._split_ext_param(ext_param)
+ F = self.feedforward.probe(z, ext_param_ff)
+ B = self.feedback.probe(z, ext_param_fb)
N = F.shape[0]
I = torch.eye(N, dtype=F.dtype, device=F.device)
A = I - F @ B
@@ -643,8 +644,10 @@
Used for numerical stability when :math:`|z| < 1` (evaluate in w-plane).
"""
ext_param_ff, ext_param_fb = self._split_ext_param(ext_param)
- probe_ff = getattr(self.feedforward, 'probe_w', None) or self.feedforward.probe
- probe_fb = getattr(self.feedback, 'probe_w', None) or self.feedback.probe
+ probe_ff = getattr(self.feedforward, 'probe_w', None)
+ probe_fb = getattr(self.feedback, 'probe_w', None)
+ if probe_ff is None or probe_fb is None:
+ raise NotImplementedError("probe_w not implemented for feedforward or feedback branches")
F = probe_ff(w, ext_param_ff)
B = probe_fb(w, ext_param_fb)
N = F.shape[0]
@@ -868,8 +871,16 @@
**Returns**:
torch.Tensor: Transfer matrix (complex).
"""
- H_A = self.branchA.probe(z, ext_param)
- H_B = self.branchB.probe(z, ext_param)
+ ext_param_brA = None
+ ext_param_brB = None
+ if ext_param is not None:
+ for key, param in ext_param.items():
+ if 'branchA' in key:
+ ext_param_brA = param
+ elif 'branchB' in key:
+ ext_param_brB = param
+ H_A = self.branchA.probe(z, ext_param_brA)
+ H_B = self.branchB.probe(z, ext_param_brB)
if self.sum_output:
return H_A + H_B
else:Implement probe_w on parallelSOSFilter to evaluate H(1/w) (w = z^{-1}) using mapped coefficients and the alias envelope, returning a diagonal transfer matrix. Update Series.probe_w to prefer a module's probe_w and fall back to module.probe(1.0/w) for z-domain-only modules so the series result correctly represents H(1/w). This ensures correct w-domain probing for SOS blocks when used in series with delays or recursions.
In Recursion (flamo/processor/system.py) replaced explicit torch.log(torch.linalg.det(P)) calls with torch.logdet(P) for succinctness and potential numeric/performance benefits. Added probe_recursion_w(w, ext_param=None) to evaluate the characteristic matrix in the w-domain (P(w) = I - B(w) F(w)) and provide a stable alternative to calling probe_recursion(1/w) when modules expose probe_w. Also minor whitespace cleanup on a return line.
Remove the optional ext_param argument from probe and probe_w methods across the processor package and adapt callers accordingly. Probes now always use self.param (no external parameter routing), and Series/Parallel/Recursion/Shell call module.probe/probe_w without passing ext_param. Recursion's _split_ext_param helper and external-parameter routing were removed; characteristic evaluation and derivative helpers were updated to match the simplified API. This change consolidates probe behavior and simplifies probe call sites and implementations.
Replace the handwritten Wirtinger derivative reconstruction with PyTorch native complex autograd. Introduce _ensure_complex_tensor and rename/replace API: wirtinger_derivative -> complex_derivative and wirtinger_derivative_scalar -> complex_derivative_scalar, implemented using torch.autograd.grad + conj. Update probe_with_derivative and all imports/usages in system.py and tests to use the new helpers, and remove the manual real/imag gradient bookkeeping for improved simplicity and stability.
Remove built-in complex autograd derivative utilities and related APIs/tests. The probe module no longer exports complex_derivative, complex_derivative_scalar, probe_with_derivative, or internal _ensure_complex_tensor; only probe_points remains. Recursion.probe_recursion was simplified to always return P(z) (derivative handling removed) and docstrings updated to state that callers (e.g. pyFDN) should construct derivatives via torch.autograd.functional.jvp/grad. Tests that asserted/verified the removed derivative helpers and Recursion derivative behavior were deleted or updated to match the simplified API. Updated imports and docstrings across flamo/processor to reflect these API changes.
Remove private helpers _eval_characteristic and _eval_characteristic_w and inline their logic into probe_recursion and probe_recursion_w. Compute the characteristic matrix P = I - F @ B directly in the probe methods, update docstrings for clarity, and use probe_w fallback selection for numerical-stability probing in the w-plane. This refactor removes duplication and clarifies intent without changing runtime behavior.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| B = self.feedback.probe(z) | ||
| N = F.shape[0] | ||
| I = torch.eye(N, dtype=F.dtype, device=F.device) | ||
| return I - F @ B |
There was a problem hiding this comment.
Docstring claims wrong matrix multiplication order in probe
High Severity
The probe_recursion docstring states P(z) = I - B(z) F(z) and references "delays on columns," but the code actually computes I - F @ B (i.e., I - feedforward @ feedback). This is consistent with Recursion.forward and Recursion.probe (which both use I - F @ B), but the documented formula has the matrix multiplication order swapped. Since the docstring explicitly says callers (e.g., pyFDN) will use this for JVP/grad-based derivatives of P(z), the mismatch between documented and actual matrix structure will lead to incorrect results for any caller relying on the documented convention. The same issue appears in probe_recursion_w.
Additional Locations (1)
| B = self.feedback.probe(z) | ||
| N = F.shape[0] | ||
| I = torch.eye(N, dtype=F.dtype, device=F.device) | ||
| return I - F @ B |
There was a problem hiding this comment.
Unused parameter include_shell_io in probe_recursion
Low Severity
probe_recursion accepts an include_shell_io parameter and **kwargs, but neither is used in the method body. A caller passing include_shell_io=True would expect behavior similar to Shell.probe, but the argument is silently ignored. This is misleading dead code.



Implement native arbitrary z-plane probing and autograd derivatives for FLAMO modules to enable advanced transfer-matrix analysis.
Note
Medium Risk
Adds new
probe/probe_wAPIs and matrix-composition logic across DSP building blocks; while runtime processing paths are largely unchanged, incorrect probe implementations orNone-returning modules can lead to shape/dtype issues in transfer-matrix analysis.Overview
Adds native arbitrary z-plane transfer-matrix probing across FLAMO DSP modules by introducing
probe(z)/probe_w(w)on theDSPbase and implementing concrete evaluations for key blocks (e.g.Gain,Filter/parallelFilter,SOSFiltervariants, andDelayvariants usingH(z)=\gamma^m z^{-m}).Extends system containers to compose these probes:
Seriesnow multiplies per-module transfer matrices,Recursioncan probe the closed-loop transfer and exposes characteristic-matrix probing (probe_recursion*),Parallelcombines branch transfer matrices, andShellcan optionally include input/output layer probes;Transform/FFT/iFFTadd no-op probe stubs for compatibility.Written by Cursor Bugbot for commit c0897ba. This will update automatically on new commits. Configure here.