Skip to content

Add Chua circuit examples, alpha-transform dynamics, and core WDF extensions - #5

Open
frantic0 wants to merge 23 commits into
gusanthon:mainfrom
frantic0:main
Open

Add Chua circuit examples, alpha-transform dynamics, and core WDF extensions#5
frantic0 wants to merge 23 commits into
gusanthon:mainfrom
frantic0:main

Conversation

@frantic0

Copy link
Copy Markdown

This pull request adds support for simulating the Chua circuit in pywdf and introduces a small set of extensions to the core WDF API, along with new examples and documentation updates.

Core API changes (pywdf/core/wdf.py)

  • Add ChuaDiode, a nonlinear negative-resistance element implementing a piecewise-linear I–V characteristic.
  • Add SeriesVoltage, a two-port series adaptor with an embedded resistive voltage source for injecting voltage impulses (used to excite the Chua circuit, but reusable in other circuits).
  • Implement an α-transform discretisation option for linear dynamic elements (Capacitor and Inductor), with parameters for selecting between bilinear (Tustin) and backward-Euler behaviour and exploring different stability/accuracy trade-offs.
  • Add a base-class helper method to convert wave variables to currents, following the WDF parametric definition.

New examples

  • Add two WDF Chua circuit examples:
    • Chua circuit excited by a resistive voltage source.
    • Chua circuit excited via the new SeriesVoltage adaptor.
  • Add basic RLC example circuits: resistor, resistor_series, resistor_parallel, inductor, and capacitor.

Documentation

  • Add a reference in README.md to the Meerkötter & Scholz paper describing the digital simulation of nonlinear circuits by WDF principles.

Testing

  • Verified that the new core elements and helpers can be instantiated and used in WDF circuits without errors.
  • Ran existing example scripts plus the new Chua and basic-circuit examples to confirm they execute and produce reasonable outputs.

@xaviliz

xaviliz commented Nov 21, 2025

Copy link
Copy Markdown
Collaborator

Hi @frantic0

Thanks a lot for your contribution.
It looks like a really interesting example to include in the library.
It is really exciting that others enlarge this library.

Unfortunately, I am very busy these days.
We are at the end of the course :)
But we have start to revise it.

Hopefully we can give you some feedback soon.

@frantic0

Copy link
Copy Markdown
Author

Thanks so much for the reply @xaviliz, and no worries at all, I completely understand how busy the end of a course can be (it’s a hectic time on my side too!).

I really appreciate you taking the time to look at this. I also wanted to say how much I enjoy pywdf, it’s a fantastic project and a great resource for exploring WDFs in practice.

Happy to make any changes or adjustments once you’ve had a chance to review.

@gusanthon

gusanthon commented Nov 26, 2025

Copy link
Copy Markdown
Owner

Hey @frantic0 , thanks so much for the contribution this is great work! Just added a few minor comments but otherwise looks great!

@frantic0

Copy link
Copy Markdown
Author

Hi @gusanthon @xaviliz, I hope you’re both enjoying a good seasonal break. Just checking in on the state of the PR, I’m very happy to make any changes you’d like before it’s ready to merge. Do let me know, please.

@gusanthon

Copy link
Copy Markdown
Owner

Hey @frantic0 , sorry for the late reply! I left a couple small comments in the review, after that I think it's ready to merge. Thanks again for the contribution!

@frantic0

frantic0 commented Mar 6, 2026

Copy link
Copy Markdown
Author

Hi @gusanthon thanks, glad that the contribution was found valuable. I can't seem to find any review or comments... Am I missing something?

Comment thread pywdf/examples/inductor.py Outdated
# self.SW1 = Switch(self.S1)

# init and set circuit
super().__init__(self.Vs, self.S1, self.C1)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be L1 instead of C1?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct inductor variable in constructor argument

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! changed

Comment thread pywdf/examples/resistor_parallel.py Outdated
self.R2.set_resistance(new_R)


def process_sample(self, sample: float) -> float:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting a ValueError running this, not sure we need to override process_sample here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, we don't need it. I removed it and fixed the problem

Comment thread pywdf/examples/voltage_divider.py Outdated
self.R2.set_resistance(new_R)


def process_sample(self, sample: float) -> float:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same ValueError here, don't think process_sample needs overriding

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same problem, we don't need it. I removed it and fixed the problem

Comment thread pywdf/core/circuit.py Outdated



def process_sample_i_v(self, sample: float) -> float:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def process_sample_i_v(self, sample: float) -> float:
def process_sample_i_v(self, sample: float) -> tuple[float, float, float]:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct type anotation to tuple of floats

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected type annotation

@gusanthon

Copy link
Copy Markdown
Owner

Hi yes @frantic0 sorry about that, looks like I didnt submit the review. Hopefully you can see it now. Thansk again!

frantic0 and others added 4 commits March 6, 2026 21:31
correct return type annotation to tuple[float, float, float]

Co-authored-by: gusanthon <99610799+gusanthon@users.noreply.github.com>
@frantic0

frantic0 commented Mar 7, 2026

Copy link
Copy Markdown
Author

hi @gusanthon, thanks a lot for the review and suggestions! I’ve gone through and addressed all the comments in the latest commits. Please let me know if you’d like any further changes.

Comment thread test.py
@@ -1,14 +1,25 @@
from pywdf import RCA_MK2_SEF, TR_808_HatResonator, DiodeClipper
from pywdf import RCA_MK2_SEF, TR_808_HatResonator, DiodeClipper, VoltageDivider, Chua

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test.py is failing, looks like Chua is not exported in __init__.py
also seems like the Chua constructor takes different args

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed Chua export in init.py and Chua constructor in test.py

import math
import numpy as np

from pywdf.core.solver.newton_raphson import newton_raphson

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_newton_raphson.py fails - I don't see newton_rapshon in the solver module?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a small sys.path bootstrap at the top of tests/test_newton_raphson.py and tests/runge_kutta.py to allow direct execution

Comment thread pywdf/examples/inductor.py Outdated
if self.frequency != frequency:
self.frequency = frequency

self.L = 1.0 / (np.square(self.twopi * frequency) * self.C)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is causing AttributeError: '_Inductor' object has no attribute 'C'

@gusanthon

Copy link
Copy Markdown
Owner

I'm also getting a path issue when plotting in each of voltage_divider.py, resistor_series.py, resistor_parallel.py:
FileNotFoundError: [Errno 2] No such file or directory: '../../tests/resistor_parallel.png'

I also noticed chua_ODE.py in the repo structure but not in the source.

Thanks again for the updates!

@frantic0

Copy link
Copy Markdown
Author

Hi @gusanthon @xaviliz , hope you had a good Easter break!

I pushed a few corrections in to fix the issues you pointed out. I realised that I had some dependencies from classes that were only on my develop branch (like the solvers) which got roped in when I first did the PR. I merged them onto the main and fixed paths that weren't working, and improved some of the examples too.

Let me know if there any other issues or changes required. Thanks for checking and reviewing!

@frantic0

Copy link
Copy Markdown
Author

Hi @gusanthon, any feedback on the latest set of changes?

@gusanthon gusanthon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @frantic0 , sorry for the slow reply again! Went through this properly and ran it all locally, the OpenCircuit fix is a nice catch.

Just a couple things I think need fixing before this goes in - the ResistiveVoltageSource arg change and the IdealCurrentSource reflected wave. The alpha transform also doesn't look finished to me, the coefficients get calculated but nothing seems to use them. Not a regression since the default matches the old behavior, but wanted to ask what you'd rather do with it.

Also, pywdf/figures/diode_clipper_transient_anal.png gets deleted here but the readme still points at it, so that image will be broken once this merges. Might just be a stray deletion? Don't worry about the generated pngs and csvs under tests/, I'll clean that up and update the gitignore after this lands.

Everything else I spotted is minor and I can sort it out myself. Thanks again for your patience with this one!

Comment thread pywdf/core/wdf.py Outdated
class ResistiveVoltageSource(baseWDF):
def __init__(self, Rval: float = None):
# def __init__(self, Rval: float = None):
def __init__(self, next: baseWDF = None, Rval: float = None) -> None:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding next as the first arg breaks the callers that pass Rval positionally. tr_808_hatresonator.py:20 and rc_highpass.py:27 both end up with Rval = 1e-09 instead of 22e3 and 75, no error, they just run with the wrong source impedance now. is next used anywhere in the class?

(old signature's still commented out just above too)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't rembember exactly what I was trying to do here, but I reverted this. Haven't had the chance to test it yet though.

Comment thread pywdf/core/wdf.py
def accept_incident_wave(self, a: float) -> None:
self.a = a

def propagate_reflected_wave(self) -> float:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think this is missing the port resistance? 1mA into a 1k resistor gives me 0.001V instead of 1.0V. should it be 2.0 * Rp * self.Is + self.a? examples/inductor.py:27 uses this so that one's off too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, IdealCurrentSource was missing the port resistance indeed. I implemented it by calculating the port resistance from the child node port impedance and the expression 2.0 * Rp * self.Is + self.a

I also improved Inductor and Capacitor circuit examples with a more meaningful configuration and tests for alpha=0.0. this alpha config should now be the default for the reactance examples.

Also Generated plots for the alpha parameter in both reactances capacitor_alpha_x.x.png inductor_alpha_x.x.png. That definitely attests the usefulness of the alpha transform :)

Comment thread pywdf/core/wdf.py
self.Rp = 1.0 / ( ( 1.0 + self.alpha ) * self.C * self.fs)
self.G = 1.0 / self.Rp

def accept_incident_wave(self, a: float) -> None:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is alpha doing anything here? a_coef / b_coef get set in setAlpha but propagate_reflected_wave just below is still self.b = self.z, so I get b=1 at alpha 1.0, 0.5 and 0.0 and only Rp changes. should the state stored here be a_coef * a + b_coef * b? that way alpha=0 gives you b = v[n-1] rather than b = a[n-1].

Inductor looks the same, and there I think the two lines cancel out, b_coef + a_coef is always 1 so it reduces to self.b = -self.z for any alpha.

No strong feelings on how you handle it, happy either way if you'd rather finish it here or drop the alpha arg for now and add it in a later PR. Only thing I'd avoid is leaving it half wired up, since passing alpha=0.5 would quietly give you the wrong dynamics.

(if you do touch Inductor, its __init__ isn't calling baseWDF.__init__, so setAlpha throws AttributeError: 'Inductor' object has no attribute 'parent')

@frantic0 frantic0 Jul 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I started the implementation but forgot to complete the propagate_reflected_wave accordingly for the capacitor. the inductor was almost complete, just missing a comment.

now the coefficients effect both reactances' reflected wave. If alpha=1.0 (bilinear transform) it reduces to the default behaviours (self.b = self.z and self.b = -self.z, for capacitor and inductor respectively) otherwise we get a ratio of coefficients of a + b in the reflected wave

well spotted, added baseWDF.init(self) to Inductor init

frantic0 added 5 commits July 27, 2026 23:59
…ild node port impedance and 2.0 * Rp * self.Is + self.a

Improved Inductor and Capacitor circuit examples with a more meaningful configuration and tests for alpha=0.0. this alpha config should be the default for the reactance examples

Generated plots for alpha parameter capacitor_alpha_x.x.png inductor_alpha_x.x.png
@frantic0

Copy link
Copy Markdown
Author

Hi @gusanthon,

Thanks for taking the time to review carefully and run everything locally.

I believe I’ve now addressed all the issues you raised, including the ResistiveVoltageSource argument change, the IdealCurrentSource reflected wave, and the alpha-transform implementation. I’ve replied to each review thread with details of the corresponding changes. Please let me know whether the corrections look sufficient.

Regarding pywdf/figures/diode_clipper_transient_anal.png, that was indeed an accidental deletion. The image had previously been generated by the root-level test.py, and I commented out that part before the previous push. I’ve restored the image referenced by the README and updated the diode_clipper.py example so that newly generated test output is written to the tests directory instead.

Please let me know if there is anything else you would like me to adjust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants