Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xspice simulation #76

Closed
matthuszagh opened this issue Jan 14, 2020 · 17 comments
Closed

xspice simulation #76

matthuszagh opened this issue Jan 14, 2020 · 17 comments

Comments

@matthuszagh
Copy link

Thanks for this great program!

How can I perform an ngspice, xspice simulation with skidl? Here's a simple example (buffer driven by a sinusoidal voltage source) that works using just PySpice.

#!/usr/bin/env python

from PySpice.Spice.Netlist import Circuit
from PySpice.Unit import *
from PySpice.Spice.NgSpice.Shared import NgSpiceShared

ngspice = NgSpiceShared.new_instance()
circuit = Circuit("Circuit")

circuit.SinusoidalVoltageSource(
    "vsource",
    "vin",
    circuit.gnd,
    amplitude=1.65 @ u_V,
    offset=1.65 @ u_V,
    frequency=100e6,
)
circuit.A("adc", "[vin]", "[buf_in]", model="adc")
circuit.model(
    "adc",
    "adc_bridge",
    in_low=1.6,
    in_high=1.7,
    rise_delay=1e-9,
    fall_delay=1e-9,
)
circuit.A("buf", "buf_in", "buf_out", model="buf")
circuit.model(
    "buf", "d_buffer", rise_delay=1e-9, fall_delay=1e-9, input_load=1e-12,
)
circuit.A("dac", "[buf_out]", "[vout]", "dac")
circuit.model("dac", "dac_bridge", out_low=0, out_high=3.3)
circuit.R("R", "vout", circuit.gnd, 1 @ u_kOhm)

simulator = circuit.simulator()
analysis = simulator.transient(step_time=0.1 @ u_ns, end_time=50 @ u_ns)

for (vin, vout) in zip(
    analysis["vin"].as_ndarray(), analysis["vout"].as_ndarray()
):
    print("{:.3f}\t{:.2f}".format(vin, vout))

However, I'm having trouble translating this to the equivalent skidl syntax. From the documentation/code I see that A() is used for this, but I don't see how I can set the ports and model with this. Here's an example of something I've tried, although unsurprisingly it doesn't work.

#!/usr/bin/env python

from skidl.pyspice import *

# component declaration
vin = sinev(offset=1.65 @ u_V, amplitude=1.65 @ u_V, frequency=100e6)
adc = A("adc", "[vin]", "[buf_in]", model="adc")
adc_model = Part(
    "pyspice",
    "adc",
    "adc_bridge",
    in_low=1.6,
    in_high=1.7,
    rise_delay=1e-9,
    fall_delay=1e-9,
)
buf = A("buf", "buf_in", "buf_out", model="buf")
buf_model = Part(
    "pyspice",
    "buf",
    "d_buffer",
    rise_delay=1e-9,
    fall_delay=1e-9,
    input_load=1e-12,
)
dac = A("dac", "[buf_out]", "[vout]", "dac")
dac_model = Part("pyspice", "dac", "dac_bridge", out_low=0, out_high=3.3)
r = R(value=1 @ u_kOhm)

# component connections
vin["n"] += gnd, r["n"]
vin["p"] += adc["[vin]"]
adc["[buf_in]"] += buf["buf_in"]
buf["buf_out"] += dac["[buf_out]"]
r["p"] += dac["[vout]"]

circ = generate_netlist()
sim = circ.simulator()
waveforms = sim.transient(step_time=0.1 @ u_ns, end_time=50 @ u_ns)
time = waveforms.time
vin = waveforms[node(vin["p"])]
vout = waveforms[node(r["p"])]

print(time)
print(vin)
print(vout)

It complains starting at the adc=A(... line, which I guess is expected since the only valid attribute is model. However, I'm confused about how to actually define this model, and also how to define the ports. Thanks.

@xesscorp
Copy link
Collaborator

I think the A() PySPice construct became more fully-realized after I built the interface into SKiDL. I'll need to make some modifications. Thanks for the example. That will help guide me.

@xesscorp
Copy link
Collaborator

I made some modifications to support XSPICE components in SKiDL. You can try them out by installing:

pip install git+https://github.com/xesscorp/skidl

Here's your example rewritten using the new SKiDL features:

from skidl.pyspice import *

# component declaration
vin = sinev(offset=1.65 @ u_V, amplitude=1.65 @ u_V, frequency=100e6)
adc = A("[anlg_in]", "[dig_out]", model=XspiceModel("adc", "adc_bridge", in_low=0.05 @ u_V, in_high=0.1 @ u_V, rise_delay=1e-9 @ u_s, fall_delay=1e-9 @ u_s))
buf = A("buf_in", "buf_out", model=XspiceModel("buf", "d_buffer", rise_delay=1e-9 @ u_s, fall_delay=1e-9 @ u_s, input_load=1e-12 @ u_s,))
dac = A("[dig_in]", "[anlg_out]", model=XspiceModel("dac", "dac_bridge", out_low=1.0 @ u_V, out_high=3.3 @ u_V))
r = R(value=1 @ u_kOhm)

# component connections
vin["n"] += gnd, r["n"]
vin["p"] += adc["anlg_in"][0]  # Attach to first pin in ADC anlg_in vector of pins.
adc["dig_out"][0] += buf["buf_in"]  # Attach first pin of ADC dig_out vector to buffer.
buf["buf_out"] += dac["dig_in"][0]  # Attach buffer output to first pin of DAC dig_in vector of pins.
r["p"] += dac["anlg_out"][0]  # Attach DAC first pin of anlg_out vector to load resistor.

circ = generate_netlist(libs='SpiceLib')
print(circ)
sim = circ.simulator()
waveforms = sim.transient(step_time=0.1 @ u_ns, end_time=50 @ u_ns)
time = waveforms.time
vin = waveforms[node(vin["p"])]
vout = waveforms[node(r["p"])]

print(time)
print(vin)
print(vout)

Here are the primary modifications to SKiDL:

  1. You can define an XSPICE model using the XspiceModel() function. The first argument is the name of an instance of the model. The second argument is the name of the XSPICE model. The remaining keyword arguments are parameters that will be inserted into the .model statement.
  2. A new XSPICE part is created using the A() or xspice() function. The arguments to this function are strings naming I/O pins that will be created for the part. A plain string like some_input will create a single pin with that name that you can use like any normal SKiDL pin. A string enclosed in brackets like [some_vector] will create a PinList object that can contain one or more pins that you'll access using zero-based indexing. The final argument is a keyword argument (model) which will pass in an XspiceModel object for the particular device you're creating.
  3. Attaching to non-vector pins of XSPICE parts is done the same way as with other SKiDL parts: just use the name of the pin assigned in the A(...) function call. For vector pins, you'll need to append a further zero-based index to indicate which member of the vector your referring to. You don't need to explicitly create these pins: they'll be created whenever you reference them. Just be careful because if you did something like gnd += some_part[some_vector][9], then some_vector would be allocated with 10 Pins in it.

One caveat on this is I haven't been able to get this to simulate correctly: even though ngspice doesn't complain about any problems, all the outputs of the XSPICE components are stuck at zero. However, this is also true when I tried running your PySpice file, so I suspect it's something to do with my ngspice executable. I did compare the SPICE netlist output by my program to the one from your PySpice program and they were topologically the same (i.e., the node names were different), so I have hope that this will actually function once the ngspice problem is solved. Let me know if this works for you.

I think that's about it.

@matthuszagh
Copy link
Author

I'm getting the following backtrace when running your example:

python file.py
F6 50 99 POLY(1) V6 300U 1
F6 50 99 POLY(1) V6 300U 1

No errors or warnings found during netlist generation.

Traceback (most recent call last):
  File "file.py", line 54, in <module>
    circ = generate_netlist(libs="/home/matt/src/spicelib")
  File "/nix/store/l37l3xnj4iqkzncqyn83ahlcd4r2h3z1-python3-3.7.6-env/lib/python3.7/site-packages/skidl/Circuit.py", line 411, in generate_netlist
    self.backup_parts()  # Create a new backup lib for the circuit parts.
  File "/nix/store/l37l3xnj4iqkzncqyn83ahlcd4r2h3z1-python3-3.7.6-env/lib/python3.7/site-packages/skidl/Circuit.py", line 577, in backup_parts
    lib += p
  File "/nix/store/l37l3xnj4iqkzncqyn83ahlcd4r2h3z1-python3-3.7.6-env/lib/python3.7/site-packages/skidl/SchLib.py", line 119, in add_parts
    self.parts.append(part.copy(dest=TEMPLATE))
  File "/nix/store/l37l3xnj4iqkzncqyn83ahlcd4r2h3z1-python3-3.7.6-env/lib/python3.7/site-packages/skidl/Part.py", line 410, in copy
    cpy += [p.copy() for p in self.pins]  # Add pin and its attribute.
  File "/nix/store/l37l3xnj4iqkzncqyn83ahlcd4r2h3z1-python3-3.7.6-env/lib/python3.7/site-packages/skidl/Part.py", line 491, in add_pins
    pin.part = self
AttributeError: 'NoneType' object has no attribute 'part'

This seems to happen with the "adc" xspice model (self.xspice_model.name is adc when running through pdb). Is this not happening for you?

As for your issue with xspice, did you enable xspice support when compiling ngspice (--enable-xspice)?

@matthuszagh
Copy link
Author

I was able to bypass that issue temporarily with do_backup=False. However, I'm now getting all zeros too... What version of Ngspice are you using? PySpice is telling me my version (31) is unsupported. It appears that the latest supported version is 30. If you're also on 31 maybe that's the issue? I'm going to open a feature request there for support.

@xesscorp
Copy link
Collaborator

Yeah, sorry. Looks like I introduced an error at the end when I was trying t support Part copying. I'll look into it and see what's going on.

Are you getting all-zero outputs even with your original PySpice version?

I'm using ngspice 30 under Windows 10. Everything I've read says you don't need the --enable-xspice on Windows because it's already compiled with it enabled.

@xesscorp
Copy link
Collaborator

I fixed the problem with SKiDL. My example doesn't throw an error for me, anymore.

XSPICE simulation still returns all zeroes.

@matthuszagh
Copy link
Author

Great, thanks for fixing. I'm currently trying to figure out the pyspice issue/reproduce what I had before. I'll post back with updates.

@matthuszagh
Copy link
Author

This appears to be a bug with Ngspice (in particular, I believe its the same as this one). I was able to get the correct behavior with my old version of Ngspice (30, maybe you have a later commit, I don't know).

If you're curious about the intended results, the netlist is something like this

.tran 0.1ns 50ns
*
.title
A1 [N1] [N2] adc
A2 N2 N3 buf
A3 [N3] [N4] dac
R1 N4 0 1kOhm
V1 N1 0 DC 0V AC SIN(1.65V 1.65V 100000000.0Hz 0s 0Hz)
.model adc adc_bridge (fall_delay=1e-09s in_high=0.1V in_low=0.05V rise_delay=1e-09s)
.model buf d_buffer (fall_delay=1e-09s input_load=1e-12s rise_delay=1e-09s)
.model dac dac_bridge (out_high=3.3V out_low=1.0V)
*
.end

This line is the problem:

V1 N1 0 DC 0V AC SIN(1.65V 1.65V 100000000.0Hz 0s 0Hz)

Changing it to

V1 N1 0 DC 0V AC 1 SIN(1.65V 1.65V 100000000.0Hz 0s 0Hz)

or

V1 N1 0 SIN(1.65V 1.65V 100000000.0Hz 0s 0Hz)

Should give you the correct output. In other words, as far as I can tell, skidl is doing the right thing here.

@xesscorp
Copy link
Collaborator

That may need fixing in ngspice, but it also looks like PySpice might need some modification. I can't see why it's putting the AC keyword in there if there are no parameters for it. All the AC and DC stuff could be handled by the SIN() any way. Or you could get rid of the SIN() entirely by using the DC and AC keywords for the standard voltage source (except these aren't implemented for the BasicElement.VoltageSource). I'll bet the same problem occurs with the standard current source as well. Some work on PySpice might get around this problem for all versions of ngspice.

That said, do you think the current way that SKiDL handles XSPICE parts is adequate? Do you see the need for any improvements/additions?

@matthuszagh
Copy link
Author

You’re right that some of what PySpice emits is unnecessary. However, I looked at the Ngspice manual and technically it’s in accordance with the syntax. For instance, the ac magnitude is assumed to be 1 if omitted, which is the case here. Since I haven’t spent much time looking through the code, maybe the author has a reason for it that I don’t know.

In regard to the way skidl handles xspice it seems good so far, but I haven’t used it much yet. Let me use it to model some more stuff and then I’ll make another post with thoughts.

Thanks for all the work you’ve put into this and for implementing the xspice feature so quickly.

@matthuszagh
Copy link
Author

Is it possible to use the Part-style interface for xspice? For example,

from skidl.pyspice import Part, XspiceModel
adc = Part(
    "pyspice",
    "A",
    "[anlg_in]",
    "[dig_out]",
    model=XspiceModel(
        "adc",
        "adc_bridge",
        in_low=3.3 / 3,
        in_high=2 * 3.3 / 3,
        rise_delay=1e-9,
        fall_delay=1e-9,
    ),
)

Fails with

ValueError: Unable to find part A in library pyspice.

@xesscorp
Copy link
Collaborator

I've checked in the updated SKiDL. You should be able to instantiate XSPICE parts using the A(...) or Part('pyspice', 'A', ...) notation as shown in the code below.

from skidl.pyspice import *

# component declaration
vin = sinev(offset=1.65 @ u_V, amplitude=1.65 @ u_V, frequency=100e6)
adc = Part('pyspice', 'A', io="[anlg_in],[dig_out]", model=XspiceModel("adc", "adc_bridge", in_low=0.05 @ u_V, in_high=0.1 @ u_V, rise_delay=1e-9 @ u_s, fall_delay=1e-9 @ u_s))
buf = A(io=["buf_in, buf_out"], model=XspiceModel("buf", "d_buffer", rise_delay=1e-9 @ u_s, fall_delay=1e-9 @ u_s, input_load=1e-12 @ u_s,))
dac = A(io=["[dig_in]", "[anlg_out]"], model=XspiceModel("dac", "dac_bridge", out_low=1.0 @ u_V, out_high=3.3 @ u_V))
r = R(value=1 @ u_kOhm)

# component connections
vin["n"] += gnd, r["n"]
vin["p"] += adc["anlg_in"][0]  # Attach to first pin in ADC anlg_in vector of pins.
adc["dig_out"][0] += buf["buf_in"]  # Attach first pin of ADC dig_out vector to buffer.
buf["buf_out"] += dac["dig_in"][0]  # Attach buffer output to first pin of DAC dig_in vector of pins.
r["p"] += dac["anlg_out"][0]  # Attach DAC first pin of anlg_out vector to load resistor.

circ = generate_netlist(libs='SpiceLib')
print(circ)
sim = circ.simulator()
waveforms = sim.transient(step_time=0.1 @ u_ns, end_time=50 @ u_ns)
time = waveforms.time
vin = waveforms[node(vin["p"])]
vout = waveforms[node(r["p"])]

print(time)
print(vin)
print(vout)

@matthuszagh
Copy link
Author

matthuszagh commented Jan 28, 2020

Works great, thanks! I'll continue to test this out and will provide feedback if anything comes up, but I've tried this out with a few simulations and so far everything works very well and the interface is easy to use.

@xesscorp
Copy link
Collaborator

Do you see any other needed modifications before I close the issue and document the feature?

@matthuszagh
Copy link
Author

I'm wondering a bit about the interface to vector pins. The current method feels a bit counter-intuitive. For instance, if I declare something like

xor = Part(
    "pyspice",
    "A",
    io=["[in1 in2]", "out"],
    model=XspiceModel(
        "xor", "d_xor", rise_delay=1e-12, fall_delay=1e-12, input_load=1e-12,
    ),
)

I would then access in2 with

xor["in1"][1]
# or
xor["in2"][1]

whereas when writing the netlist directly in spice I would just write the internal node name directly to make a connection to the pin. In that vein, it might be nice to be able to access the pins in a flat way. I.e.

xor["in2"]

I currently don't see any use to being able to access the pin in a vector-like format. However, I'm very new to xspice, so that use case may be obvious to someone with more experience. From what I can tell reading the documentation, it seems like the vector format is mostly useful for declaring the pin interface but is not used after that. Do you have any experience with this? Any thoughts?

@xesscorp
Copy link
Collaborator

Yeah, you can't name vector I/O using names like [in1 in2]. Vector I/O is used to create inputs and outputs that don't have a specific number of pins. Logic gates are a perfect example: you want a library with a single XOR gate that can handle any number of inputs, not a library with a bunch of XOR gates with each one handling a specific number of inputs (2, 3, 4, etc.). Vector I/O never has a specific number of elements that you can list individually. All you can do is assign a name to the vector and then access individual pins of the vector using array notation. There is an alternate syntax that may make it more intuitive: io=["in[]", "out"]. Then you would access the first input as xor["in"][0] and the second input as xor["in"][1] (and reference the scalar output as xor["out"]).

When you created the vector input using "[in1 in2]", what you actually did was create a single vector input with the name in1 in2. Then when you accessed it using xor["in1"][1], what SKiDL did was look for a pin named in1 but there wasn't anything with that exact name. So it resorted to a regular expression match and found in1 matched a part of the pin named in1 in2 and it accepted that. This match was actually for a PinList object, so the following [1] index caused SKiDL to create two pins for the input vector and then return the second of those pins (the first pin would have had index 0). The same process would have occurred for xor["in2"][1] and you would have accessed the exact same pin. And if you used xor["in1"] or xor["in2"], you would have gotten the entire PinList named in1 in2 instead of an individual pin.

@matthuszagh
Copy link
Author

That makes a lot more sense, and now I see the benefit of the vector notation. Thanks for clearing that up. I have no further feedback at this time. Feel free to close and document.

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

No branches or pull requests

2 participants