Add Butterworth zeta constructor for uniform damping ratio#57
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Butterworth constructor overload that accepts zeta, computes zeta-specific continuous-time transfer functions, generates new zeta reference data, and extends runtime and compile-time Butterworth tests. ChangesButterworth Uniform-Zeta Constructor
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@include/constfilt/butterworth.hpp`:
- Around line 192-204: Reject negative damping ratios in
butterworth_poly_coeffs_zeta before constructing the transfer function: validate
zeta with the existing constexpr-friendly stability path or CheckStab template
parameter and fail for zeta < 0 while still allowing zeta == 0. Update the
butterworth_poly_coeffs_zeta logic (and any constructor or helper that calls it)
so unstable inputs are filtered out consistently with the library’s Stability
handling, using the existing symbols CheckStab, Stability, and
butterworth_poly_coeffs_zeta.
In `@tests/butterworth.test.cpp`:
- Around line 339-341: The ZOH Butterworth test coverage is documenting an
unsupported N>=4 path instead of fixing it; update the relevant Butterworth/ZOH
implementation so repeated-eigenvalue cases work for arbitrary order, and then
expand the tests in Butterworth<T, N, ZOH> to cover N>=4 rather than limiting
them to N<=3. Use the Butterworth constructor/discretization path and any
eigenvalue/consteig handling it relies on, and remove the implicit order ceiling
so only the required static_assert(N >= 1) remains.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1b625d88-9386-4060-8cba-7f173649f6b3
📒 Files selected for processing (5)
include/constfilt/butterworth.hppoctave/generate_butterworth_tests.mtests/butterworth.test.cpptests/butterworth_reference.hpptests/constexpr_asserts.cpp
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
- GitHub Check: cross.arm-gcc
- GitHub Check: clang
- GitHub Check: gcc
- GitHub Check: cross.arm-clang
- GitHub Check: cross.arm-clang
- GitHub Check: clang
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{hpp,h,cpp}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{hpp,h,cpp}: Use C++17 only; do not use C++20 features (no std::is_constant_evaluated, no concepts)
Enforce clang-format code formatting via CI; run make container.make.format before committing
Files:
include/constfilt/butterworth.hpptests/constexpr_asserts.cpptests/butterworth.test.cpp
include/constfilt/**
📄 CodeRabbit inference engine (AGENTS.md)
Maintain header-only library structure; do not add .cpp translation units to the library itself
Files:
include/constfilt/butterworth.hpp
**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
Implement constexpr everywhere possible; coefficient computation must work at compile time; Filter::operator() batch path must remain constexpr
Files:
include/constfilt/butterworth.hpp
include/constfilt/**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
include/constfilt/**/*.{hpp,h}: Do not introduce hard-coded filter-order ceiling; Filter classes should only use static_assert(N >= 1)
Use unsigned int for template parameter order N; implicit conversion with consteig::Size (size_t) is acceptable; do not add casts unless compiler error requires it
Follow a[0] = 1 convention for discrete transfer functions (monic-denominator); Filter stores _a with a[0] included for uniformity but DF2T equations assume a[0] = 1
Implement stability check via CheckStab template parameter (default true) to reject Stability::Unstable systems while accepting Stable and MarginallyStable; check not yet implemented (issue#14)
Use DirectForm II Transposed (DF2T) implementation for runtime filter with both sample-by-sample real-time path and constexpr-capable batch path
Support Butterworth filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options
Support Elliptic filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options; use Nome-series Cauer design (see docs/elliptic.md)
Files:
include/constfilt/butterworth.hpp
**/*.hpp
⚙️ CodeRabbit configuration file
**/*.hpp: This is a constexpr C++17 header-only filter design library. Focus on:
- Correctness of constexpr computations and static_assert usage
- Type consistency (float vs double tolerances must match the filter's numeric type)
- No standard library dependencies in core library code (freestanding constraint)
- Correct use of vendored consteig (matrix ops) and gcem (math functions) via the wrapper
- Headers named *_reference.hpp contain precomputed test data — do not suggest algorithmic changes to their values
Files:
include/constfilt/butterworth.hpp
**/tests/**
⚙️ CodeRabbit configuration file
**/tests/**: These are compile-time and runtime tests. Check:
- static_assert failures = build failures = test failures (treat equally)
- Tests are split into individual .cpp files intentionally to avoid constexpr budget exhaustion
- Do not suggest merging test files
- Reference values live in *_reference.hpp headers — do not suggest hardcoding tolerances inline
Files:
tests/constexpr_asserts.cpptests/butterworth.test.cpp
octave/**
⚙️ CodeRabbit configuration file
octave/**: GNU Octave/MATLAB scripts for test generation and algorithm prototyping.
Generated reference output is checked in — do not suggest regenerating unless filter parameters change.
Files:
octave/generate_butterworth_tests.m
🔇 Additional comments (3)
octave/generate_butterworth_tests.m (1)
221-369: LGTM!tests/butterworth.test.cpp (1)
344-350: LGTM!Also applies to: 352-410, 418-484
tests/constexpr_asserts.cpp (1)
719-737: LGTM!
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/butterworth.md`:
- Around line 129-141: Clarify the supported zeta range in the pole-placement
section: the current formula in butterworth.md only applies to the underdamped
case handled by the pole placement description, so either state that the
constructor assumes 0 <= zeta <= 1 or expand the “Pole placement” explanation to
include the overdamped/repeated-real-pole case when zeta > 1. Update the
surrounding text near the “Pole placement” heading and the p = ωc(-zeta ± j√(1 -
zeta^2)) formula so it matches the behavior of the Butterworth constructor.
In `@include/constfilt/butterworth.hpp`:
- Around line 55-56: The Butterworth compile-time check is using a std type
trait in a std-free area; replace the std::is_same<Method, ZOH> check in
butterworth.hpp with the internal tag-trait pattern used by analog_filter.hpp.
Update the static_assert to rely on the local trait for identifying ZOH instead
of any std:: namespace dependency, keeping include/constfilt self-contained.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 86a2ea79-58c9-460b-9e20-749ff9ca23c0
📒 Files selected for processing (2)
docs/butterworth.mdinclude/constfilt/butterworth.hpp
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
- GitHub Check: gcc
- GitHub Check: cross.arm-clang
- GitHub Check: clang
- GitHub Check: cross.arm-gcc
- GitHub Check: cross.arm-gcc
- GitHub Check: gcc
- GitHub Check: cross.arm-clang
- GitHub Check: clang
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{hpp,h,cpp}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{hpp,h,cpp}: Use C++17 only; do not use C++20 features (no std::is_constant_evaluated, no concepts)
Enforce clang-format code formatting via CI; run make container.make.format before committing
Files:
include/constfilt/butterworth.hpp
include/constfilt/**
📄 CodeRabbit inference engine (AGENTS.md)
Maintain header-only library structure; do not add .cpp translation units to the library itself
Files:
include/constfilt/butterworth.hpp
**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
Implement constexpr everywhere possible; coefficient computation must work at compile time; Filter::operator() batch path must remain constexpr
Files:
include/constfilt/butterworth.hpp
include/constfilt/**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
include/constfilt/**/*.{hpp,h}: Do not introduce hard-coded filter-order ceiling; Filter classes should only use static_assert(N >= 1)
Use unsigned int for template parameter order N; implicit conversion with consteig::Size (size_t) is acceptable; do not add casts unless compiler error requires it
Follow a[0] = 1 convention for discrete transfer functions (monic-denominator); Filter stores _a with a[0] included for uniformity but DF2T equations assume a[0] = 1
Implement stability check via CheckStab template parameter (default true) to reject Stability::Unstable systems while accepting Stable and MarginallyStable; check not yet implemented (issue#14)
Use DirectForm II Transposed (DF2T) implementation for runtime filter with both sample-by-sample real-time path and constexpr-capable batch path
Support Butterworth filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options
Support Elliptic filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options; use Nome-series Cauer design (see docs/elliptic.md)
Files:
include/constfilt/butterworth.hpp
**/*.hpp
⚙️ CodeRabbit configuration file
**/*.hpp: This is a constexpr C++17 header-only filter design library. Focus on:
- Correctness of constexpr computations and static_assert usage
- Type consistency (float vs double tolerances must match the filter's numeric type)
- No standard library dependencies in core library code (freestanding constraint)
- Correct use of vendored consteig (matrix ops) and gcem (math functions) via the wrapper
- Headers named *_reference.hpp contain precomputed test data — do not suggest algorithmic changes to their values
Files:
include/constfilt/butterworth.hpp
🔇 Additional comments (1)
include/constfilt/butterworth.hpp (1)
52-59: 🎯 Functional CorrectnessPlease reconcile this
N <= 3uguard with the public Butterworth contract.This hard-codes an order ceiling into
include/constfilt, but the library rules for this path say Butterworth filters should support arbitrary orderN >= 1and should onlystatic_assert(N >= 1). If issue 56 really needs a temporary ZOH exception, please confirm the repo wants that exception in the public API contract before merging. As per coding guidelines, "Do not introduce hard-coded filter-order ceiling; Filter classes should only use static_assert(N >= 1)".Source: Coding guidelines
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/butterworth.md`:
- Around line 140-146: The overdamped description in the Butterworth
documentation uses the wrong radical form for zeta > 1. Update the text in the
section describing the poles so it either states the real-pole expression p =
omega_c(-zeta ± sqrt(zeta^2 - 1)) or explains that the complex form j*sqrt(1 -
zeta^2) reduces to that real form in the overdamped case, keeping the
explanation aligned with butterworth_poly_coeffs_zeta and the pole cases above
it.
In `@include/constfilt/butterworth.hpp`:
- Around line 55-59: The Butterworth zeta constructor currently imposes an
unwanted public order cap via the static_assert in Butterworth, blocking valid
Butterworth<N, ZOH> use for N >= 4. Remove the hard-coded N <= 3u ceiling from
this constructor and keep only the general filter-shape validation, then move
any ZOH repeated-pole handling into the discretization/backend path used by the
ZOH method rather than the public API. Use the Butterworth template and its zeta
constructor as the place to locate and adjust this check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3dd00020-3e1d-4f73-8cb5-5d8204cb5e06
📒 Files selected for processing (3)
docs/butterworth.mdinclude/constfilt/butterworth.hppinclude/constfilt/discretize.hpp
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
- GitHub Check: clang
- GitHub Check: gcc
- GitHub Check: cross.arm-clang
- GitHub Check: cross.arm-gcc
- GitHub Check: gcc
- GitHub Check: clang
- GitHub Check: cross.arm-gcc
- GitHub Check: cross.arm-clang
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{hpp,h,cpp}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{hpp,h,cpp}: Use C++17 only; do not use C++20 features (no std::is_constant_evaluated, no concepts)
Enforce clang-format code formatting via CI; run make container.make.format before committing
Files:
include/constfilt/discretize.hppinclude/constfilt/butterworth.hpp
include/constfilt/**
📄 CodeRabbit inference engine (AGENTS.md)
Maintain header-only library structure; do not add .cpp translation units to the library itself
Files:
include/constfilt/discretize.hppinclude/constfilt/butterworth.hpp
**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
Implement constexpr everywhere possible; coefficient computation must work at compile time; Filter::operator() batch path must remain constexpr
Files:
include/constfilt/discretize.hppinclude/constfilt/butterworth.hpp
include/constfilt/**/*.{hpp,h}
📄 CodeRabbit inference engine (AGENTS.md)
include/constfilt/**/*.{hpp,h}: Do not introduce hard-coded filter-order ceiling; Filter classes should only use static_assert(N >= 1)
Use unsigned int for template parameter order N; implicit conversion with consteig::Size (size_t) is acceptable; do not add casts unless compiler error requires it
Follow a[0] = 1 convention for discrete transfer functions (monic-denominator); Filter stores _a with a[0] included for uniformity but DF2T equations assume a[0] = 1
Implement stability check via CheckStab template parameter (default true) to reject Stability::Unstable systems while accepting Stable and MarginallyStable; check not yet implemented (issue#14)
Use DirectForm II Transposed (DF2T) implementation for runtime filter with both sample-by-sample real-time path and constexpr-capable batch path
Support Butterworth filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options
Support Elliptic filters with lowpass and highpass modes, arbitrary order (N >= 1), and ZOH or Matched-Z discretization options; use Nome-series Cauer design (see docs/elliptic.md)
Files:
include/constfilt/discretize.hppinclude/constfilt/butterworth.hpp
**/*.hpp
⚙️ CodeRabbit configuration file
**/*.hpp: This is a constexpr C++17 header-only filter design library. Focus on:
- Correctness of constexpr computations and static_assert usage
- Type consistency (float vs double tolerances must match the filter's numeric type)
- No standard library dependencies in core library code (freestanding constraint)
- Correct use of vendored consteig (matrix ops) and gcem (math functions) via the wrapper
- Headers named *_reference.hpp contain precomputed test data — do not suggest algorithmic changes to their values
Files:
include/constfilt/discretize.hppinclude/constfilt/butterworth.hpp
🔇 Additional comments (2)
include/constfilt/butterworth.hpp (1)
200-259: LGTM!include/constfilt/discretize.hpp (1)
64-72: LGTM!
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
octave/generate_butterworth_tests.m (1)
259-289: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStop emitting uniform-zeta ZOH references for orders above 3.
include/constfilt/butterworth.hppstatically rejectsButterworth(..., zeta)withconstfilt::ZOHforN > 3, but these loops still generate and check incase_zeta_*/case_hp_zeta_*fixtures for orders 4..8. That leavestests/butterworth_reference.hppdescribing a path the library intentionally does not support.Suggested fix
for ci = 1:length(zeta_lp_cases) ord = zeta_lp_cases{ci}{1}; @@ - % --- ZOH --- - sys_d = c2d(sys_c, 1.0/fs, 'zoh'); - [b_d, a_d] = tfdata(sys_d, 'v'); - while length(b_d) < length(a_d); b_d = [0, b_d]; end - y_step = filter(b_d, a_d, u); - y_imp = filter(b_d, a_d, [1, zeros(1, STEP_LEN-1)]); - y_c = filter(b_d, a_d, u_c); - emit_case(fid, sprintf('case_zeta_%d_%dHz_%dHz_%s', ord, fc, fs, zeta_tag), ... - ord, fc, fs, b_d, a_d, y_step, y_imp, u_c, y_c); + if ord <= 3 + % --- ZOH --- + sys_d = c2d(sys_c, 1.0/fs, 'zoh'); + [b_d, a_d] = tfdata(sys_d, 'v'); + while length(b_d) < length(a_d); b_d = [0, b_d]; end + y_step = filter(b_d, a_d, u); + y_imp = filter(b_d, a_d, [1, zeros(1, STEP_LEN-1)]); + y_c = filter(b_d, a_d, u_c); + emit_case(fid, sprintf('case_zeta_%d_%dHz_%dHz_%s', ord, fc, fs, zeta_tag), ... + ord, fc, fs, b_d, a_d, y_step, y_imp, u_c, y_c); + end @@ for ci = 1:length(zeta_hp_cases) ord = zeta_hp_cases{ci}{1}; @@ - % --- ZOH --- - sys_d = c2d(sys_c, 1.0/fs, 'zoh'); - [b_d, a_d] = tfdata(sys_d, 'v'); - y_step = filter(b_d, a_d, u); - y_imp = filter(b_d, a_d, [1, zeros(1, STEP_LEN-1)]); - y_c = filter(b_d, a_d, u_c); - emit_case(fid, sprintf('case_hp_zeta_%d_%dHz_%dHz_%s', ord, fc, fs, zeta_tag), ... - ord, fc, fs, b_d, a_d, y_step, y_imp, u_c, y_c); + if ord <= 3 + % --- ZOH --- + sys_d = c2d(sys_c, 1.0/fs, 'zoh'); + [b_d, a_d] = tfdata(sys_d, 'v'); + y_step = filter(b_d, a_d, u); + y_imp = filter(b_d, a_d, [1, zeros(1, STEP_LEN-1)]); + y_c = filter(b_d, a_d, u_c); + emit_case(fid, sprintf('case_hp_zeta_%d_%dHz_%dHz_%s', ord, fc, fs, zeta_tag), ... + ord, fc, fs, b_d, a_d, y_step, y_imp, u_c, y_c); + endAs per path instructions, "Generated reference output is checked in — do not suggest regenerating unless filter parameters change."
Also applies to: 322-351
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@octave/generate_butterworth_tests.m` around lines 259 - 289, Stop generating uniform-zeta ZOH reference cases for Butterworth orders above 3, since the library intentionally rejects that path in `Butterworth(..., zeta)` with `constfilt::ZOH`. Update the `zeta_lp_cases` loop in `generate_butterworth_tests.m` (and the matching `case_hp_zeta_*` loop it also applies to) so `emit_case` is only produced for supported orders, keeping `tests/butterworth_reference.hpp` aligned with the supported API. Use the existing `uniform_zeta_lp`, `sys_d = c2d(..., 'zoh')`, and `emit_case` flow, but gate out `ord > 3` rather than checking in unsupported fixtures.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@octave/generate_butterworth_tests.m`:
- Around line 259-289: Stop generating uniform-zeta ZOH reference cases for
Butterworth orders above 3, since the library intentionally rejects that path in
`Butterworth(..., zeta)` with `constfilt::ZOH`. Update the `zeta_lp_cases` loop
in `generate_butterworth_tests.m` (and the matching `case_hp_zeta_*` loop it
also applies to) so `emit_case` is only produced for supported orders, keeping
`tests/butterworth_reference.hpp` aligned with the supported API. Use the
existing `uniform_zeta_lp`, `sys_d = c2d(..., 'zoh')`, and `emit_case` flow, but
gate out `ord > 3` rather than checking in unsupported fixtures.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 251c489a-9da0-412b-a5bd-f9ee7ba1ac1e
📒 Files selected for processing (3)
octave/generate_butterworth_tests.mtests/butterworth.test.cpptests/butterworth_reference.hpp
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
octave/**
⚙️ CodeRabbit configuration file
octave/**: GNU Octave/MATLAB scripts for test generation and algorithm prototyping.
Generated reference output is checked in — do not suggest regenerating unless filter parameters change.
Files:
octave/generate_butterworth_tests.m
**/*.{hpp,h,cpp}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{hpp,h,cpp}: Use C++17 only; do not use C++20 features (no std::is_constant_evaluated, no concepts)
Enforce clang-format code formatting via CI; run make container.make.format before committing
Files:
tests/butterworth.test.cpp
**/tests/**
⚙️ CodeRabbit configuration file
**/tests/**: These are compile-time and runtime tests. Check:
- static_assert failures = build failures = test failures (treat equally)
- Tests are split into individual .cpp files intentionally to avoid constexpr budget exhaustion
- Do not suggest merging test files
- Reference values live in *_reference.hpp headers — do not suggest hardcoding tolerances inline
Files:
tests/butterworth.test.cpp
🔇 Additional comments (1)
tests/butterworth.test.cpp (1)
344-351: LGTM!Also applies to: 352-383, 384-415, 416-447, 454-461, 466-493, 498-525, 530-556
Resolves #19.
Related to #14.
Changes
The classical Butterworth constructor places poles at angles
theta_k = pi*(2k+N-1)/(2N), so each conjugate pair has a different dampingratio determined by its angle. For N=2 this gives
zeta = 1/sqrt(2), and forhigher orders the pairs span a range of zeta values, with the pair closest to
the real axis having the highest damping.
This PR adds a second constructor that accepts an explicit uniform damping ratio.
Instead of spreading poles around the left half of the unit circle at prescribed
angles, every pair is placed at the same location
wc*(-zeta +/- j*sqrt(1 - zeta^2)). The magnitude is stillwcfor theunderdamped case, but the angle is
arccos(-zeta)for all pairs rather thanvarying by pair index. This trades the maximally flat magnitude property of
classical Butterworth for direct control over transient damping behavior.
The primary use case is as a building block for a biquad cascade
(#20), where N=2 sections
with a specified zeta are composed rather than designing a single high-order
filter.
The ZOH path is restricted to N<=3 for this constructor. Uniform zeta produces
repeated complex eigenvalue pairs for N>=4, which causes the QR-based
eigendecomposition used by the generic ZOH path to return wrong results. MatchedZ
and Tustin are unaffected. See
#56 for details.
Summary by CodeRabbit