After my previous post on SPICE modelling in Python, I need to use a good support example to go up to on the fly compilation in C++. This schema will also require some changes to support more than simple nodal analysis, so this now becomes Modified Nodal Analysis with state equations.
The simple model
I won’t use the full Moog ladder model, but I’ll use the simplest model found in http://www.timstinchcombe.co.uk/synth/Moog_ladder_tf.pdf:
All the base of the transistors are set to a specific voltage instead of using a bank of resistors, the current sink at the bottom of the circuit is now perfect and we use a perfect feedback loop. This is done to have a simple netlist that can be easily parsed and then transformed in C++.
We can get the steady state for this schema from LTSpice, and this is going to be important to check that we have a good match:
From this, we can ask LTSPice also to simulate. Let’s use a 100kHz sampling over 100ms and use a 50Hz input frequency:
The missing Python pieces
We need to add a few new circuits to our SPICE modeler. The easiest one is the current source:
class Current(object): """ Class that implements a perfect current generator between two pins """ nb_pins = 2 def __init__(self, current): self.current = current def __repr__(self): return "%02.2eA between pins (%s,%s)" % (self.current, self.pins, self.pins) def update_model(self, model): pass def update_steady_state(self, state, dt): pass def update_state(self, state): pass def get_current(self, pin_index, state, steady_state): return self.current * (-1 if 0 == pin_index else 1) def get_gradient(self, pin_index_ref, pin_index, state, steady_state): return 0 def precompute(self, state, steady_state): pass
And we need the perfect amplifier. For this one, we can’t use nodal analysis, so we will ask the modeller to replace the equation by the perfect voltage amplification equation:
class VoltageGain(object): """ Class that implements a voltage gain between 4 pins, Vi+, Vi-, Vo+, Vo- """ nb_pins = 4 def __init__(self, gain): self.gain = gain def __repr__(self): return "Voltage gain between pins (%s,%s,%s,%s) overriding equation at pin %s" % (self.pins, self.pins, self.pins, self.pins, self.pins) def update_model(self, model): assert self.pins == "D" model.dynamic_pins_equation[self.pins] = (self, 0) def update_steady_state(self, state, dt): pass def update_state(self, state): pass def get_current(self, pin_index, state, steady_state): return 0 def get_gradient(self, pin_index_ref, pin_index, state, steady_state): return 0 def add_equation(self, state, steady_state, eq_number): eq = self.gain * (retrieve_voltage(state, self.pins) - retrieve_voltage(state, self.pins)) - (retrieve_voltage(state, self.pins) - retrieve_voltage(state, self.pins)) jac = np.zeros(len(state["D"])) if self.pins == "D": jac[self.pins] = self.gain if self.pins == "D": jac[self.pins] = -self.gain if self.pins == "D": jac[self.pins] = -1 if self.pins == "D": jac[self.pins] = 1 return eq, jac def precompute(self, state, steady_state): pass
Simulation in Python
Before we check the steady state of our models in Python, we need to remember that our models are not the same as LTSpice. For instance, the transistors are using a Gummel-Poon model, whereas this Python modeller uses a more simple Ebers-Moll model. This means that the results won’t match, but should still be visually close enough (I won’t really try to check is they are close enough).
Clearly, there is a bias in all the stages of around 0.1V, but thanks to the DC removal, the output is more or less the same. But what about the 50Hz signal?
We see that we still have a good match, the results seems identical, with similar behavior on the lt voltage, and of course the output.
So mission accomplished, we have a good Moog ladder prototype in Python!
We now have a reference in Python for quite a complex schema (even if it’s simplified). Our next step will move the implementation to C++, without focusing on performance just now.