symbulator 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- symbulator-0.1.0/LICENSE +21 -0
- symbulator-0.1.0/PKG-INFO +260 -0
- symbulator-0.1.0/README.md +232 -0
- symbulator-0.1.0/pyproject.toml +41 -0
- symbulator-0.1.0/setup.cfg +4 -0
- symbulator-0.1.0/symbulator/__init__.py +21 -0
- symbulator-0.1.0/symbulator/analysis.py +122 -0
- symbulator-0.1.0/symbulator/dispatch.py +49 -0
- symbulator-0.1.0/symbulator/elements.py +202 -0
- symbulator-0.1.0/symbulator/engine.py +314 -0
- symbulator-0.1.0/symbulator/equiv.py +174 -0
- symbulator-0.1.0/symbulator/laplace.py +67 -0
- symbulator-0.1.0/symbulator/si_prefix.py +90 -0
- symbulator-0.1.0/symbulator/tests/__init__.py +0 -0
- symbulator-0.1.0/symbulator/tests/test_circuits.py +178 -0
- symbulator-0.1.0/symbulator/tests/test_dispatch.py +47 -0
- symbulator-0.1.0/symbulator/tests/test_equiv.py +95 -0
- symbulator-0.1.0/symbulator/tests/test_laplace.py +57 -0
- symbulator-0.1.0/symbulator/utils.py +65 -0
- symbulator-0.1.0/symbulator.egg-info/PKG-INFO +260 -0
- symbulator-0.1.0/symbulator.egg-info/SOURCES.txt +22 -0
- symbulator-0.1.0/symbulator.egg-info/dependency_links.txt +1 -0
- symbulator-0.1.0/symbulator.egg-info/requires.txt +4 -0
- symbulator-0.1.0/symbulator.egg-info/top_level.txt +1 -0
symbulator-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roberto Perez-Franco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: symbulator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A symbolic (SymPy-based) linear-circuit simulator, ported from Roberto Perez-Franco's TI-Nspire CX II CAS program of the same name.
|
|
5
|
+
Author-email: Roberto Perez-Franco <perezfranco@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/symbulator/
|
|
8
|
+
Keywords: circuit,simulation,symbolic,sympy,electronics,spice,laplace
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: sympy>=1.13
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# symbulator (Python port)
|
|
30
|
+
|
|
31
|
+
A Python/SymPy port of **Symbulator 8**, Roberto Perez-Franco's symbolic
|
|
32
|
+
linear-circuit simulator for the TI-Nspire CX II CAS.
|
|
33
|
+
|
|
34
|
+
All of the original's analysis tools are now ported: DC, AC (phasor),
|
|
35
|
+
s-domain (Laplace), and transient analysis; Thevenin/Norton equivalents;
|
|
36
|
+
two-port parameter extraction; and the expert-mode dispatcher. See
|
|
37
|
+
**Scope** below for the handful of things that are intentionally
|
|
38
|
+
simplified relative to the calculator version, and why.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install -r requirements.txt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from symbulator import dc, ac, fd, tr, th, er, port
|
|
50
|
+
|
|
51
|
+
# 5V source through a 1k/1k voltage divider
|
|
52
|
+
res = dc("e1,1,0,5:r1,1,2,1k:r2,2,0,1k")
|
|
53
|
+
print(res.v("2")) # 5/2
|
|
54
|
+
print(res.i("r1")) # 1/400 (2.5 mA)
|
|
55
|
+
print(res["p_r1"]) # power dissipated in r1
|
|
56
|
+
|
|
57
|
+
# Series RLC driven at omega = 1000 rad/s
|
|
58
|
+
res = ac("e1,1,0,10:r1,1,2,100:l1,2,3,0.1:c1,3,0,1e-6", omega=1000)
|
|
59
|
+
print(res.v("2"))
|
|
60
|
+
print(res["z_e1"]) # input impedance seen by the source
|
|
61
|
+
|
|
62
|
+
# Thevenin equivalent between node 2 and ground
|
|
63
|
+
eq = th("e1,1,0,12:r1,1,2,4k:r2,2,0,2k", "2", "0", domain="dc")
|
|
64
|
+
print(eq.vth, eq.z, eq.pmax)
|
|
65
|
+
|
|
66
|
+
# Step response of an RC circuit, in the time domain
|
|
67
|
+
res = tr("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6", variables=["v_2"])
|
|
68
|
+
print(res["v_2"]) # 5*(1 - exp(-1000*t))*Heaviside(t)-style result
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Circuit description syntax
|
|
72
|
+
|
|
73
|
+
Unchanged from the calculator (minus the leading `:`): elements are
|
|
74
|
+
separated by `:`, fields within an element by `,`. Node `0` is ground.
|
|
75
|
+
|
|
76
|
+
| Prefix | Element | Fields |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `r` | resistor | name,n1,n2,value |
|
|
79
|
+
| `l` | inductor | name,n1,n2,value[,initial_current] |
|
|
80
|
+
| `c` | capacitor | name,n1,n2,value[,initial_voltage] |
|
|
81
|
+
| `e` | voltage source (indep. or dependent) | name,n1,n2,value |
|
|
82
|
+
| `j` | current source (indep. or dependent) | name,n1,n2,value |
|
|
83
|
+
| `o` | ideal op-amp (nullor) | name,n_plus,n_minus,n_out |
|
|
84
|
+
| `m` | mutual inductance | name,Lname1,Lname2,M |
|
|
85
|
+
| `s` | ammeter / 0V test branch | name,n1,n2 |
|
|
86
|
+
| `t` | ideal transformer | name,n1,n2,turns1,turns2 |
|
|
87
|
+
| `z,y,h,g,a,b` | grounded two-port block | name,n1,n2 (params passed separately, see below) |
|
|
88
|
+
|
|
89
|
+
The optional initial-condition field on `l`/`c` (initial inductor
|
|
90
|
+
current / capacitor voltage) is only meaningful for `fd()`/`tr()`; it's
|
|
91
|
+
ignored by `dc()`/`ac()`. Unlike the original -- which required a
|
|
92
|
+
different field count per element depending on which analysis tool was
|
|
93
|
+
running -- this port always accepts the extra field and just treats it
|
|
94
|
+
as 0 if omitted, regardless of which function you call.
|
|
95
|
+
|
|
96
|
+
**Dependent (controlled) sources** work "for free": a value field can be
|
|
97
|
+
any SymPy-parseable expression referencing other node-voltage/current
|
|
98
|
+
symbols (`v_<node>`, `i_<element>`), e.g. `e2,3,0,2*v_2` for a VCVS.
|
|
99
|
+
This mirrors how the original evaluated value strings through the
|
|
100
|
+
calculator's own expression engine.
|
|
101
|
+
|
|
102
|
+
**Unit shorthand:** both the calculator's own `'k`/`'M`/`'u`/... syntax
|
|
103
|
+
and a more Python-natural bare-suffix form (`1k`, `4.7u`, `10n`) are
|
|
104
|
+
accepted for a standalone numeric value field.
|
|
105
|
+
|
|
106
|
+
**Two-port parameters** (`z/y/h/g/a/b`) are supplied via a `params`
|
|
107
|
+
dict, since on the calculator they were either predefined variables or
|
|
108
|
+
entered interactively:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
params = {"y1": {"11": "0.001", "12": "-0.001", "21": "-0.001", "22": "0.001"}}
|
|
112
|
+
res = dc("e1,1,0,10:y1,1,2:rl,2,0,1k", params=params)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If an element's params are omitted, they're left as free symbols named
|
|
116
|
+
`<name>11`, `<name>12`, etc. (matching the original's "leave them
|
|
117
|
+
symbolic" default). Use `port()` (below) to go the other way and
|
|
118
|
+
*extract* z/y/h/g/a/b parameters from an actual sub-circuit.
|
|
119
|
+
|
|
120
|
+
## DC / AC / s-domain results
|
|
121
|
+
|
|
122
|
+
`dc()`, `ac()`, and `fd()` return a `Result` with:
|
|
123
|
+
- `res.v(node)` -- node voltage
|
|
124
|
+
- `res.i(name)` -- element/branch current
|
|
125
|
+
- `res["p_<name>"]` / `res["ap_<name>"]` -- real/apparent power (DC / AC only)
|
|
126
|
+
- `res["s_<name>"]` -- complex power (AC only)
|
|
127
|
+
- `res["z_<name>"]` / `res["r_<name>"]` -- impedance / resistance seen by a source (AC / DC only)
|
|
128
|
+
|
|
129
|
+
(The power/impedance derived quantities are DC/AC-only, matching the
|
|
130
|
+
original -- `fd()` doesn't compute them either.)
|
|
131
|
+
|
|
132
|
+
`ac()` takes a `use_rms=True` flag to switch the power convention from
|
|
133
|
+
peak-amplitude phasors (default, dividing by 2) to RMS phasors, matching
|
|
134
|
+
the original's `userms` setting.
|
|
135
|
+
|
|
136
|
+
## Thevenin / Norton: `th()` and `er()`
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
eq = th("e1,1,0,12:r1,1,2,4k:r2,2,0,2k", n1="2", n2="0", domain="dc")
|
|
140
|
+
eq.vth # open-circuit (Thevenin) voltage
|
|
141
|
+
eq.ino # short-circuit (Norton) current
|
|
142
|
+
eq.z # Req (dc) or Zeq (ac) = vth/ino
|
|
143
|
+
eq.pmax # max power transferable to a matched load
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`th()` is for **active** circuits (ones with their own independent
|
|
147
|
+
sources) -- it raises if the open-circuit voltage comes out to 0, same
|
|
148
|
+
as the original's redirect message. For a **passive** (source-free)
|
|
149
|
+
network, use `er()` instead, which injects a single 1A test current and
|
|
150
|
+
reads the equivalent resistance/impedance directly:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
req = er("r1,1,2,1k:r2,2,0,2k", n1="1", n2="0", domain="dc") # 3000
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Two-port extraction: `port()`
|
|
157
|
+
|
|
158
|
+
Extracts z/y/h/g/a/b parameters of a whole circuit between two grounded
|
|
159
|
+
ports (the inverse of feeding pre-defined parameters into a `z`/`y`/...
|
|
160
|
+
circuit *element*, described above):
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
params = port("r1,1,3,100:r2,2,3,200:r3,3,0,50", n1="1", n2="2", kind="z", domain="dc")
|
|
164
|
+
params["11"], params["12"], params["21"], params["22"]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Works the same way in AC (pass `omega=...` and `domain="ac"`).
|
|
168
|
+
|
|
169
|
+
## s-domain and transient: `fd()` and `tr()`
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from symbulator import fd, tr, t2s, s2t
|
|
173
|
+
|
|
174
|
+
# Step response of an RC low-pass, starting from rest
|
|
175
|
+
res_s = fd("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6") # s-domain answer
|
|
176
|
+
res_t = tr("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6") # inverse-Laplace'd to time domain
|
|
177
|
+
res_t["v_2"]
|
|
178
|
+
|
|
179
|
+
# Natural response of a discharging inductor with an initial condition
|
|
180
|
+
res_t = tr("l1,0,2,0.2,3:r1,2,0,100", variables=["i_l1"]) # I0=3A, L=0.2H, R=100 ohm
|
|
181
|
+
res_t["i_l1"] # 3*exp(-500*t)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`t2s()`/`s2t()` wrap SymPy's `laplace_transform`/`inverse_laplace_transform`
|
|
185
|
+
directly, for preparing a time-domain source value or hand-checking an
|
|
186
|
+
answer.
|
|
187
|
+
|
|
188
|
+
`tr(desc, variables=[...])` lets you limit which answers get
|
|
189
|
+
inverse-Laplace-transformed -- useful since that step can be slow (or
|
|
190
|
+
fail to find a closed form) for complicated expressions; omit
|
|
191
|
+
`variables` to attempt every solved node voltage and element current.
|
|
192
|
+
Any individual variable that can't be transformed is silently left out
|
|
193
|
+
of the result rather than failing the whole call.
|
|
194
|
+
|
|
195
|
+
**Simplification vs. the original:** the original auto-detected when a
|
|
196
|
+
source's value was a function of time and Laplace-transformed it for
|
|
197
|
+
you (and called out to a separate `lf\\ilaplace`/`lf\\laplace` calculator
|
|
198
|
+
library for the actual transform, which wasn't included in the document
|
|
199
|
+
this was ported from). This port skips the auto-detection: give `fd()`
|
|
200
|
+
source values already in the s-domain (e.g. `"5/s"` for a 5V step,
|
|
201
|
+
`"1"` for an impulse), using `t2s()` first if you're starting from a
|
|
202
|
+
time-domain expression. `tr()` then uses SymPy's own
|
|
203
|
+
`inverse_laplace_transform` for the reverse step, per your call on how
|
|
204
|
+
to handle the missing library.
|
|
205
|
+
|
|
206
|
+
## Expert mode: `ex()`
|
|
207
|
+
|
|
208
|
+
A single dispatcher over `dc`/`ac`/`fd`/`tr`, for callers that want to
|
|
209
|
+
pick the analysis type dynamically rather than calling a specific
|
|
210
|
+
function -- ports `ex()`. On the calculator this interactively asked
|
|
211
|
+
"1:DC 2:AC 3:FD 4:TR"; as a library there's no prompt to answer, so
|
|
212
|
+
`domain` is just a normal argument (the word, or the calculator's own
|
|
213
|
+
1-4 shorthand):
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
ex("e1,1,0,5:r1,1,2,1k:r2,2,0,1k", domain="dc")
|
|
217
|
+
ex("e1,1,0,5:r1,1,0,100", domain="ac", omega=1000) # omega required for ac
|
|
218
|
+
ex("l1,0,2,0.2,3:r1,2,0,100", domain="tr", variables=["i_l1"])
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Scope: what's simplified vs. the calculator version
|
|
222
|
+
|
|
223
|
+
- **`pf()`** is ported as a simplified, explicit-argument version (pass
|
|
224
|
+
a voltage and current phasor directly); the original's implicit
|
|
225
|
+
per-element-type sign convention, driven by reading calculator
|
|
226
|
+
variables like `v<name>`/`i<name>` automatically, wasn't replicated.
|
|
227
|
+
- **`fd()`/`tr()`** require s-domain source values up front rather than
|
|
228
|
+
auto-detecting and transforming time-domain ones (see above).
|
|
229
|
+
- **No interactive prompts anywhere** -- everything the calculator asked
|
|
230
|
+
for via `RequestStr` (analysis type, which answers to save, expert-mode
|
|
231
|
+
custom equations, two-port parameter values, etc.) is a plain function
|
|
232
|
+
argument here instead.
|
|
233
|
+
- **No `Disp` progress narration** -- the calculator printed step-by-step
|
|
234
|
+
status messages during a simulation; this port just returns the
|
|
235
|
+
answer.
|
|
236
|
+
|
|
237
|
+
## Tests
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
pytest symbulator/tests/ -v
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
41 tests across five files:
|
|
244
|
+
- `test_circuits.py` (21): DC/AC voltage & current dividers, series RLC
|
|
245
|
+
impedance, inverting/non-inverting op-amp gain, a voltage-controlled
|
|
246
|
+
voltage source, an ideal transformer, mutual inductance (with and
|
|
247
|
+
without coupling), a two-port block, derived power quantities,
|
|
248
|
+
zero-valued-capacitor handling, and parser error handling.
|
|
249
|
+
- `test_equiv.py` (9): Thevenin voltage/impedance and its cross-check
|
|
250
|
+
against directly solving with a load attached, `er()` on series/parallel
|
|
251
|
+
passive networks, `port()` z/y/a-parameter extraction (including a
|
|
252
|
+
z·y matrix-inverse consistency check and an a-parameter round trip
|
|
253
|
+
through the Phase 1 two-port element), and an AC two-port case.
|
|
254
|
+
- `test_laplace.py` (5): `t2s`/`s2t` round trips, an RC step response
|
|
255
|
+
checked numerically against the closed-form exponential, an RL natural
|
|
256
|
+
response with a nonzero initial condition checked against its
|
|
257
|
+
closed-form solution, and zero-valued-capacitor handling carried
|
|
258
|
+
into `fd()`.
|
|
259
|
+
- `test_dispatch.py` (6): `ex()` dispatch to each of the four analysis
|
|
260
|
+
modes, its numeric-shorthand domain aliases, and its error handling.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# symbulator (Python port)
|
|
2
|
+
|
|
3
|
+
A Python/SymPy port of **Symbulator 8**, Roberto Perez-Franco's symbolic
|
|
4
|
+
linear-circuit simulator for the TI-Nspire CX II CAS.
|
|
5
|
+
|
|
6
|
+
All of the original's analysis tools are now ported: DC, AC (phasor),
|
|
7
|
+
s-domain (Laplace), and transient analysis; Thevenin/Norton equivalents;
|
|
8
|
+
two-port parameter extraction; and the expert-mode dispatcher. See
|
|
9
|
+
**Scope** below for the handful of things that are intentionally
|
|
10
|
+
simplified relative to the calculator version, and why.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install -r requirements.txt
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from symbulator import dc, ac, fd, tr, th, er, port
|
|
22
|
+
|
|
23
|
+
# 5V source through a 1k/1k voltage divider
|
|
24
|
+
res = dc("e1,1,0,5:r1,1,2,1k:r2,2,0,1k")
|
|
25
|
+
print(res.v("2")) # 5/2
|
|
26
|
+
print(res.i("r1")) # 1/400 (2.5 mA)
|
|
27
|
+
print(res["p_r1"]) # power dissipated in r1
|
|
28
|
+
|
|
29
|
+
# Series RLC driven at omega = 1000 rad/s
|
|
30
|
+
res = ac("e1,1,0,10:r1,1,2,100:l1,2,3,0.1:c1,3,0,1e-6", omega=1000)
|
|
31
|
+
print(res.v("2"))
|
|
32
|
+
print(res["z_e1"]) # input impedance seen by the source
|
|
33
|
+
|
|
34
|
+
# Thevenin equivalent between node 2 and ground
|
|
35
|
+
eq = th("e1,1,0,12:r1,1,2,4k:r2,2,0,2k", "2", "0", domain="dc")
|
|
36
|
+
print(eq.vth, eq.z, eq.pmax)
|
|
37
|
+
|
|
38
|
+
# Step response of an RC circuit, in the time domain
|
|
39
|
+
res = tr("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6", variables=["v_2"])
|
|
40
|
+
print(res["v_2"]) # 5*(1 - exp(-1000*t))*Heaviside(t)-style result
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Circuit description syntax
|
|
44
|
+
|
|
45
|
+
Unchanged from the calculator (minus the leading `:`): elements are
|
|
46
|
+
separated by `:`, fields within an element by `,`. Node `0` is ground.
|
|
47
|
+
|
|
48
|
+
| Prefix | Element | Fields |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `r` | resistor | name,n1,n2,value |
|
|
51
|
+
| `l` | inductor | name,n1,n2,value[,initial_current] |
|
|
52
|
+
| `c` | capacitor | name,n1,n2,value[,initial_voltage] |
|
|
53
|
+
| `e` | voltage source (indep. or dependent) | name,n1,n2,value |
|
|
54
|
+
| `j` | current source (indep. or dependent) | name,n1,n2,value |
|
|
55
|
+
| `o` | ideal op-amp (nullor) | name,n_plus,n_minus,n_out |
|
|
56
|
+
| `m` | mutual inductance | name,Lname1,Lname2,M |
|
|
57
|
+
| `s` | ammeter / 0V test branch | name,n1,n2 |
|
|
58
|
+
| `t` | ideal transformer | name,n1,n2,turns1,turns2 |
|
|
59
|
+
| `z,y,h,g,a,b` | grounded two-port block | name,n1,n2 (params passed separately, see below) |
|
|
60
|
+
|
|
61
|
+
The optional initial-condition field on `l`/`c` (initial inductor
|
|
62
|
+
current / capacitor voltage) is only meaningful for `fd()`/`tr()`; it's
|
|
63
|
+
ignored by `dc()`/`ac()`. Unlike the original -- which required a
|
|
64
|
+
different field count per element depending on which analysis tool was
|
|
65
|
+
running -- this port always accepts the extra field and just treats it
|
|
66
|
+
as 0 if omitted, regardless of which function you call.
|
|
67
|
+
|
|
68
|
+
**Dependent (controlled) sources** work "for free": a value field can be
|
|
69
|
+
any SymPy-parseable expression referencing other node-voltage/current
|
|
70
|
+
symbols (`v_<node>`, `i_<element>`), e.g. `e2,3,0,2*v_2` for a VCVS.
|
|
71
|
+
This mirrors how the original evaluated value strings through the
|
|
72
|
+
calculator's own expression engine.
|
|
73
|
+
|
|
74
|
+
**Unit shorthand:** both the calculator's own `'k`/`'M`/`'u`/... syntax
|
|
75
|
+
and a more Python-natural bare-suffix form (`1k`, `4.7u`, `10n`) are
|
|
76
|
+
accepted for a standalone numeric value field.
|
|
77
|
+
|
|
78
|
+
**Two-port parameters** (`z/y/h/g/a/b`) are supplied via a `params`
|
|
79
|
+
dict, since on the calculator they were either predefined variables or
|
|
80
|
+
entered interactively:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
params = {"y1": {"11": "0.001", "12": "-0.001", "21": "-0.001", "22": "0.001"}}
|
|
84
|
+
res = dc("e1,1,0,10:y1,1,2:rl,2,0,1k", params=params)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If an element's params are omitted, they're left as free symbols named
|
|
88
|
+
`<name>11`, `<name>12`, etc. (matching the original's "leave them
|
|
89
|
+
symbolic" default). Use `port()` (below) to go the other way and
|
|
90
|
+
*extract* z/y/h/g/a/b parameters from an actual sub-circuit.
|
|
91
|
+
|
|
92
|
+
## DC / AC / s-domain results
|
|
93
|
+
|
|
94
|
+
`dc()`, `ac()`, and `fd()` return a `Result` with:
|
|
95
|
+
- `res.v(node)` -- node voltage
|
|
96
|
+
- `res.i(name)` -- element/branch current
|
|
97
|
+
- `res["p_<name>"]` / `res["ap_<name>"]` -- real/apparent power (DC / AC only)
|
|
98
|
+
- `res["s_<name>"]` -- complex power (AC only)
|
|
99
|
+
- `res["z_<name>"]` / `res["r_<name>"]` -- impedance / resistance seen by a source (AC / DC only)
|
|
100
|
+
|
|
101
|
+
(The power/impedance derived quantities are DC/AC-only, matching the
|
|
102
|
+
original -- `fd()` doesn't compute them either.)
|
|
103
|
+
|
|
104
|
+
`ac()` takes a `use_rms=True` flag to switch the power convention from
|
|
105
|
+
peak-amplitude phasors (default, dividing by 2) to RMS phasors, matching
|
|
106
|
+
the original's `userms` setting.
|
|
107
|
+
|
|
108
|
+
## Thevenin / Norton: `th()` and `er()`
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
eq = th("e1,1,0,12:r1,1,2,4k:r2,2,0,2k", n1="2", n2="0", domain="dc")
|
|
112
|
+
eq.vth # open-circuit (Thevenin) voltage
|
|
113
|
+
eq.ino # short-circuit (Norton) current
|
|
114
|
+
eq.z # Req (dc) or Zeq (ac) = vth/ino
|
|
115
|
+
eq.pmax # max power transferable to a matched load
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`th()` is for **active** circuits (ones with their own independent
|
|
119
|
+
sources) -- it raises if the open-circuit voltage comes out to 0, same
|
|
120
|
+
as the original's redirect message. For a **passive** (source-free)
|
|
121
|
+
network, use `er()` instead, which injects a single 1A test current and
|
|
122
|
+
reads the equivalent resistance/impedance directly:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
req = er("r1,1,2,1k:r2,2,0,2k", n1="1", n2="0", domain="dc") # 3000
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Two-port extraction: `port()`
|
|
129
|
+
|
|
130
|
+
Extracts z/y/h/g/a/b parameters of a whole circuit between two grounded
|
|
131
|
+
ports (the inverse of feeding pre-defined parameters into a `z`/`y`/...
|
|
132
|
+
circuit *element*, described above):
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
params = port("r1,1,3,100:r2,2,3,200:r3,3,0,50", n1="1", n2="2", kind="z", domain="dc")
|
|
136
|
+
params["11"], params["12"], params["21"], params["22"]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Works the same way in AC (pass `omega=...` and `domain="ac"`).
|
|
140
|
+
|
|
141
|
+
## s-domain and transient: `fd()` and `tr()`
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from symbulator import fd, tr, t2s, s2t
|
|
145
|
+
|
|
146
|
+
# Step response of an RC low-pass, starting from rest
|
|
147
|
+
res_s = fd("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6") # s-domain answer
|
|
148
|
+
res_t = tr("e1,1,0,5/s:r1,1,2,1000:c1,2,0,1e-6") # inverse-Laplace'd to time domain
|
|
149
|
+
res_t["v_2"]
|
|
150
|
+
|
|
151
|
+
# Natural response of a discharging inductor with an initial condition
|
|
152
|
+
res_t = tr("l1,0,2,0.2,3:r1,2,0,100", variables=["i_l1"]) # I0=3A, L=0.2H, R=100 ohm
|
|
153
|
+
res_t["i_l1"] # 3*exp(-500*t)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`t2s()`/`s2t()` wrap SymPy's `laplace_transform`/`inverse_laplace_transform`
|
|
157
|
+
directly, for preparing a time-domain source value or hand-checking an
|
|
158
|
+
answer.
|
|
159
|
+
|
|
160
|
+
`tr(desc, variables=[...])` lets you limit which answers get
|
|
161
|
+
inverse-Laplace-transformed -- useful since that step can be slow (or
|
|
162
|
+
fail to find a closed form) for complicated expressions; omit
|
|
163
|
+
`variables` to attempt every solved node voltage and element current.
|
|
164
|
+
Any individual variable that can't be transformed is silently left out
|
|
165
|
+
of the result rather than failing the whole call.
|
|
166
|
+
|
|
167
|
+
**Simplification vs. the original:** the original auto-detected when a
|
|
168
|
+
source's value was a function of time and Laplace-transformed it for
|
|
169
|
+
you (and called out to a separate `lf\\ilaplace`/`lf\\laplace` calculator
|
|
170
|
+
library for the actual transform, which wasn't included in the document
|
|
171
|
+
this was ported from). This port skips the auto-detection: give `fd()`
|
|
172
|
+
source values already in the s-domain (e.g. `"5/s"` for a 5V step,
|
|
173
|
+
`"1"` for an impulse), using `t2s()` first if you're starting from a
|
|
174
|
+
time-domain expression. `tr()` then uses SymPy's own
|
|
175
|
+
`inverse_laplace_transform` for the reverse step, per your call on how
|
|
176
|
+
to handle the missing library.
|
|
177
|
+
|
|
178
|
+
## Expert mode: `ex()`
|
|
179
|
+
|
|
180
|
+
A single dispatcher over `dc`/`ac`/`fd`/`tr`, for callers that want to
|
|
181
|
+
pick the analysis type dynamically rather than calling a specific
|
|
182
|
+
function -- ports `ex()`. On the calculator this interactively asked
|
|
183
|
+
"1:DC 2:AC 3:FD 4:TR"; as a library there's no prompt to answer, so
|
|
184
|
+
`domain` is just a normal argument (the word, or the calculator's own
|
|
185
|
+
1-4 shorthand):
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
ex("e1,1,0,5:r1,1,2,1k:r2,2,0,1k", domain="dc")
|
|
189
|
+
ex("e1,1,0,5:r1,1,0,100", domain="ac", omega=1000) # omega required for ac
|
|
190
|
+
ex("l1,0,2,0.2,3:r1,2,0,100", domain="tr", variables=["i_l1"])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Scope: what's simplified vs. the calculator version
|
|
194
|
+
|
|
195
|
+
- **`pf()`** is ported as a simplified, explicit-argument version (pass
|
|
196
|
+
a voltage and current phasor directly); the original's implicit
|
|
197
|
+
per-element-type sign convention, driven by reading calculator
|
|
198
|
+
variables like `v<name>`/`i<name>` automatically, wasn't replicated.
|
|
199
|
+
- **`fd()`/`tr()`** require s-domain source values up front rather than
|
|
200
|
+
auto-detecting and transforming time-domain ones (see above).
|
|
201
|
+
- **No interactive prompts anywhere** -- everything the calculator asked
|
|
202
|
+
for via `RequestStr` (analysis type, which answers to save, expert-mode
|
|
203
|
+
custom equations, two-port parameter values, etc.) is a plain function
|
|
204
|
+
argument here instead.
|
|
205
|
+
- **No `Disp` progress narration** -- the calculator printed step-by-step
|
|
206
|
+
status messages during a simulation; this port just returns the
|
|
207
|
+
answer.
|
|
208
|
+
|
|
209
|
+
## Tests
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
pytest symbulator/tests/ -v
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
41 tests across five files:
|
|
216
|
+
- `test_circuits.py` (21): DC/AC voltage & current dividers, series RLC
|
|
217
|
+
impedance, inverting/non-inverting op-amp gain, a voltage-controlled
|
|
218
|
+
voltage source, an ideal transformer, mutual inductance (with and
|
|
219
|
+
without coupling), a two-port block, derived power quantities,
|
|
220
|
+
zero-valued-capacitor handling, and parser error handling.
|
|
221
|
+
- `test_equiv.py` (9): Thevenin voltage/impedance and its cross-check
|
|
222
|
+
against directly solving with a load attached, `er()` on series/parallel
|
|
223
|
+
passive networks, `port()` z/y/a-parameter extraction (including a
|
|
224
|
+
z·y matrix-inverse consistency check and an a-parameter round trip
|
|
225
|
+
through the Phase 1 two-port element), and an AC two-port case.
|
|
226
|
+
- `test_laplace.py` (5): `t2s`/`s2t` round trips, an RC step response
|
|
227
|
+
checked numerically against the closed-form exponential, an RL natural
|
|
228
|
+
response with a nonzero initial condition checked against its
|
|
229
|
+
closed-form solution, and zero-valued-capacitor handling carried
|
|
230
|
+
into `fd()`.
|
|
231
|
+
- `test_dispatch.py` (6): `ex()` dispatch to each of the four analysis
|
|
232
|
+
modes, its numeric-shorthand domain aliases, and its error handling.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "symbulator"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A symbolic (SymPy-based) linear-circuit simulator, ported from Roberto Perez-Franco's TI-Nspire CX II CAS program of the same name."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Roberto Perez-Franco", email = "perezfranco@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["circuit", "simulation", "symbolic", "sympy", "electronics", "spice", "laplace"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
|
28
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"sympy>=1.13",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest>=7.0"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://pypi.org/project/symbulator/"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["symbulator", "symbulator.*"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
symbulator: a Python/SymPy port of Roberto Perez-Franco's "Symbulator 8"
|
|
3
|
+
TI-Nspire CX II CAS circuit simulator.
|
|
4
|
+
|
|
5
|
+
Phase 1 scope: DC and AC (phasor) analysis of linear circuits built from
|
|
6
|
+
resistors, inductors, capacitors, independent/dependent sources, ideal
|
|
7
|
+
op-amps, mutual inductance, ideal transformers, and grounded two-port
|
|
8
|
+
(z/y/h/g/a/b) blocks.
|
|
9
|
+
|
|
10
|
+
Not yet ported: s-domain (fd) / transient (tr) analysis, Thevenin/Norton
|
|
11
|
+
(th), two-port parameter extraction (port), equivalent-resistance (er),
|
|
12
|
+
expert mode (ex).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .analysis import dc, ac, fd
|
|
16
|
+
from .utils import pr, pf, gain
|
|
17
|
+
from .equiv import th, er, port
|
|
18
|
+
from .laplace import tr, t2s, s2t
|
|
19
|
+
from .dispatch import ex
|
|
20
|
+
|
|
21
|
+
__all__ = ["dc", "ac", "fd", "tr", "t2s", "s2t", "pr", "pf", "gain", "th", "er", "port", "ex"]
|