### Analog modelling: The Moog ladder filter emulation in Python

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++.

# SPICE simulation

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:

 V(in+) 0 V(in-) -3.94938e-05 V(lt) -0.528279 V(out) 0.000118481 V(s1+) 2.47182 V(s1-) 2.47178 V(s2+) 3.47188 V(s2-) 3.47185 V(s3+) 4.47195 V(s3-) 4.47191 V(s4+) 5.47201 V(s4-) 5.47197

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

pass

def update_state(self, state):
pass

return self.current * (-1 if 0 == pin_index else 1)

return 0

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)

pass

def update_state(self, state):
pass

return 0

return 0

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

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).

 V(in+) 0 V(in-) -3.95065e-05 V(lt) -0.66857 V(out) 0.00011852 V(s1+) 2.33155 V(s1-) 2.33151 V(s2+) 3.33163 V(s2-) 3.33159 V(s3+) 4.3317 V(s3-) 4.33167 V(s4+) 5.33178 V(s4-) 5.33174

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?