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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"]