diff --git a/Makefile b/Makefile index c53863cc..e8e9db8e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: install install: #python3 setup.py install - pip3 install . + pip3 install -e . .PHONY: install-extras install-extras: diff --git a/TODO b/TODO index b3e47a95..4781a00f 100644 --- a/TODO +++ b/TODO @@ -54,6 +54,7 @@ singular matrix. 4. Handle controlled sources for loop and mesh analysis + Infrastructure ============== diff --git a/doc/examples/schematics/grounds.png b/doc/examples/schematics/grounds.png index 0c91d401..d27390d6 100644 Binary files a/doc/examples/schematics/grounds.png and b/doc/examples/schematics/grounds.png differ diff --git a/doc/examples/schematics/grounds.sch b/doc/examples/schematics/grounds.sch index 85b3a6ef..bc713abf 100644 --- a/doc/examples/schematics/grounds.sch +++ b/doc/examples/schematics/grounds.sch @@ -26,3 +26,4 @@ W 6 7; right W 7 07; down=0.2, 0V A 7; l=0V, anchor=s ; label_nodes=none +; draw_nodes=none diff --git a/doc/examples/schematics/grounds2.png b/doc/examples/schematics/grounds2.png new file mode 100644 index 00000000..fa208481 Binary files /dev/null and b/doc/examples/schematics/grounds2.png differ diff --git a/doc/examples/schematics/grounds2.sch b/doc/examples/schematics/grounds2.sch new file mode 100644 index 00000000..50e0a665 --- /dev/null +++ b/doc/examples/schematics/grounds2.sch @@ -0,0 +1,17 @@ +# tailless ground +W 1 01; down=0.2, tlground +A 1; l=tlground, anchor=s +W 1 2; right +# thick tailless ground +W 2 02; down=0.2, tground +A 2; l=tground, anchor=s +W 2 3; right +# European ground +W 3 03; down=0.2, eground +A 3; l=eground, anchor=s +W 3 4; right +# alternative European ground +W 4 04; down=0.2, eground2 +A 4; l=eground2, anchor=s +; label_nodes=none +; draw_nodes=none diff --git a/doc/examples/schematics/transistors.png b/doc/examples/schematics/transistors.png index 7ceec300..14697557 100644 Binary files a/doc/examples/schematics/transistors.png and b/doc/examples/schematics/transistors.png differ diff --git a/doc/examples/schematics/transistors.sch b/doc/examples/schematics/transistors.sch index c15e14ee..9c01c0fa 100644 --- a/doc/examples/schematics/transistors.sch +++ b/doc/examples/schematics/transistors.sch @@ -29,5 +29,7 @@ M15 40 41 42 M15; up=1.5, kind=nfet, l=nfet/bodydiode, bodydiode M16 43 44 42 M16; up=1.5, kind=pfet, l=pfet/bodydiode, bodydiode M17 43 45 46 M17; up=1.5, kind=nmosd, l=nmosd/bulk, bulk M18 47 48 46 M18; up=1.5, kind=pmosd, l=pmosd/bulk, bulk +M19 47 49 50 M19; up=1.5, kind=nmos, l=nmos/arrowmos, arrowmos +M20 51 52 50 M20; up=1.5, kind=pmos, l=pmos/arrowmos, arrowmos ; label_nodes=none, draw_nodes=connections diff --git a/doc/examples/schematics/transistors3.png b/doc/examples/schematics/transistors3.png index 6cee1529..7cb945cc 100644 Binary files a/doc/examples/schematics/transistors3.png and b/doc/examples/schematics/transistors3.png differ diff --git a/doc/netlists.rst b/doc/netlists.rst index 746c955d..2405a589 100644 --- a/doc/netlists.rst +++ b/doc/netlists.rst @@ -497,6 +497,20 @@ Circuit methods referred to when the last switch activated prior to the time specified for `t`. +- `defs(ignore)` Returns a directory of argname-value pair for all + components except for components with names specified by + `ignore`. For example, + + >>> cct = Circuit(""" + ... R 1 2 3 + ... C1 2 3 4 + ... C2 3 4 5 6 + ... L 4 5 7 8""") + >>> cct.defs() + {'R': '3', 'C1': '4', 'C2': '5', 'C2_v0': '6', 'L': '7', 'L_i0': '8'} + + See also `sympify()` and `subs()`. + - `describe()` Prints message describing how netlist is solved - `evidence_matrix()` Returns the evidence matrix. This has a size @@ -541,7 +555,7 @@ Circuit methods >>> a.initialize({'L1': 7}) L1 1 2 L1 7 - See also `convert_IVP`. + See also `convert_IVP()`. - `open_circuit(cpt)` Applies open circuit in series with the component. The name of the open circuit component is returned. @@ -568,7 +582,7 @@ Circuit methods - `replace_switches(t)` Replaces switches with a short-circuit or open-circuit circuit by considering whether the specified time `t` - is at or after the switch activation time. See also `convert_IVP`. + is at or after the switch activation time. See also `convert_IVP()`. >>> cct = Circuit(""" ... SW1 1 2 no @@ -585,7 +599,7 @@ Circuit methods - `replace_switches_before(t)` Replaces switches with a short-circuit or open-circuit circuit by considering whether the specified time - `t` is before the switch activation time. See also `convert_IVP`. + `t` is before the switch activation time. See also `convert_IVP()`. - `s_model()` Converts sources to the s-domain and represents reactive components as impedances. @@ -632,6 +646,27 @@ Circuit methods >>> cct.switching_times() [0.0, 1.0, 2.0] +- `sympify(ignore)` Converts numerical values to symbolic values + except for component names specified by `ignore`. For example, + + >>> cct = Circuit(""" + ... R 1 2 3 + ... C1 2 3 4 + ... C2 3 4 5 6 + ... L 4 5 7 8""") + >>> cct.sympify() + R 1 2 + C1 2 3 + C2 3 4 C2 v0_C2 + L 4 5 L i0_L + >>> cct.sympify().subs(cct.defs()) + R 1 2 3 + C1 2 3 4 + C2 3 4 5 6 + L 4 5 7 8 + + Note how the initial values are named. See also `defs()` and `subs()`. + - `unconnected_nodes` Returns list of names of nodes that are unconnected - `unreachable_nodes(node)` Returns list of names of nodes that have no path to `node`. diff --git a/doc/schematics.rst b/doc/schematics.rst index a5b8572d..70e2a2f2 100644 --- a/doc/schematics.rst +++ b/doc/schematics.rst @@ -825,7 +825,7 @@ MOSFET transistors have the following attributes: - `bodydiode` -- `arrow` +- `arrowmos` - `ferroel gate` ferroelectric gate @@ -978,6 +978,10 @@ connections. They have one of the following attributes: - `nground` noiseless ground - `pground` protected ground - `rground` reference ground +- `tlground` tailless ground +- `tground` thicker tailless ground +- `eground` European style ground +- `eground2` alternative European style ground - `0V` ground - `vcc` positive power supply (voltage to collectors) - `vdd` positive power supply (voltage to drains ;-) @@ -1011,6 +1015,11 @@ Here are some ground examples: .. image:: examples/schematics/grounds.png :width: 15cm +.. literalinclude:: examples/schematics/grounds2.sch + +.. image:: examples/schematics/grounds2.png + :width: 7.5cm + Here are some power supply examples: diff --git a/lcapy/mnacpts.py b/lcapy/mnacpts.py index 2abd8921..e84384d4 100644 --- a/lcapy/mnacpts.py +++ b/lcapy/mnacpts.py @@ -57,6 +57,7 @@ class Cpt(ImmittanceMixin): is_wire = False is_current_controlled = False is_voltage_controlled = False + extra_argnames = () def __init__(self, cct, namespace, name, cpt_type, cpt_id, string, opts_string, node_names, keyword, *args): @@ -679,6 +680,9 @@ def I(self): """Current through component. The current is defined to be into the positive node.""" + if self.ignore: + raise ValueError('Cannot determine I for %s' % self) + return self.cct.get_I(self.name) @property @@ -700,6 +704,9 @@ def p(self): def V(self): """Voltage drop across component.""" + if self.ignore: + raise ValueError('Cannot determine V for %s' % self) + return self.cct.get_Vd(self.nodes[0].name, self.nodes[1].name) @property @@ -897,6 +904,36 @@ def short_circuit(self): return self.cct.last_added() + @property + def argnames(self): + + """Return list of arg names.""" + + args = [self.name] + args.extend([self.name + '_' + name for name in self.extra_argnames]) + + return args + + def defs(self): + """Return directory of argname-value pair.""" + + defs = {} + for argname, value in zip(self.argnames, self.args): + if value is None: + # Initial condition for C and L. + continue + defs[argname] = value + + return defs + + def sympify(self): + """Return component with numerical values removed.""" + + if len(self.args) == 1: + return self._netmake(args=[]) + + return self._netmake(args=self.argnames) + class Invalid(Cpt): pass @@ -937,14 +974,26 @@ class Dummy(Cpt): has_ic = None noisy = False + def _stamp(self, mna): + pass -class XX(Dummy): - is_directive = True +class Ignore(Dummy): + ignore = True - def _stamp(self, mna): - pass + @property + def Voc(self): + raise ValueError('Cannot determine Voc for %s' % self) + + @property + def Isc(self): + raise ValueError('Cannot determine Isc for %s' % self) + + +class XX(Ignore): + + is_directive = True def _subs(self, subs_dict): return self._copy() @@ -958,10 +1007,6 @@ def __str__(self): return self._string -class A(Cpt): - pass - - class AM(Cpt): # Model ammeter as a 0 V voltage source so can determine @@ -1110,6 +1155,7 @@ class C(RC): is_reactive = True add_parallel = True + extra_argnames = ('v0', ) @property def C(self): @@ -1169,6 +1215,14 @@ def _ss_model(self): # with another voltage source? return self._netmake_variant('V_', args='v_%s(t)' % self.relname) + def sympify(self): + """Return component with numerical values removed.""" + + if self.has_ic: + return self._netmake(args=self.argnames) + + return self._netmake(args=[]) + @property def V0(self): """Initial voltage (for capacitors only).""" @@ -1180,6 +1234,8 @@ def V0(self): class CPE(RC): + extra_argnames = ('alpha',) + @property def K(self): return self.cpt.K @@ -1245,6 +1301,8 @@ class E(VCVS): class Eopamp(DependentSource): """Operational amplifier""" + extra_argnames = ('Ac', 'Ro') + def _expand(self): Ad, Ac, Ro = self.args @@ -1282,6 +1340,8 @@ def _stamp(self, mna): class Efdopamp(DependentSource): """Fully differential opamp""" + extra_argnames = ('Ac') + def _expand(self): Ad, Ac = self.args @@ -1311,6 +1371,8 @@ def _stamp(self, mna): class Einamp(DependentSource): """Instrumentation amplifier""" + extra_argnames = ('Ac', 'Rf') + def _expand(self): Ad, Ac, Rf = self.args @@ -1627,6 +1689,7 @@ class L(RLC): need_branch_current = True is_reactive = True add_series = True + extra_argnames = ('i0', ) def _r_model(self): @@ -1701,24 +1764,28 @@ def _stamp(self, mna): V = self.Voc.sympy mna._Es[m] += V + def _pre_initial_model(self): + + return self._netmake_variant('I', args=self.cpt.i0) + def _ss_model(self): # Perhaps mangle name to ensure it does not conflict # with another current source? return self._netmake_variant('I_', args='i_%s(t)' % self.relname) - def _pre_initial_model(self): + def sympify(self): + """Return component with numerical values removed.""" - return self._netmake_variant('I', args=self.cpt.i0) + if self.has_ic: + return self._netmake(args=self.argnames) + return self._netmake(args=[]) class O(Dummy): """Open circuit""" is_open_circuit = True - def _stamp(self, mna): - pass - @property def I(self): return SuperpositionCurrent(0) @@ -1733,9 +1800,6 @@ class P(Dummy): is_port = True - def _stamp(self, mna): - pass - @property def I(self): return SuperpositionCurrent(0) @@ -1763,9 +1827,31 @@ def _r_model(self): class RV(RC): - # TODO. Can simulate as series resistors (1 - alpha) R and alpha R. - pass + def _stamp(self, mna): + + n1, n2, n3 = mna._cpt_node_indexes(self) + + R = expr(self.args[0]).sympy + a = expr(self.args[1]).sympy + + Y1 = 1 / (R * (1 - a)) + Y2 = 1 / (R * a) + + if n1 >= 0 and n3 >= 0: + mna._G[n1, n3] -= Y1 + mna._G[n3, n1] -= Y1 + if n1 >= 0: + mna._G[n1, n1] += Y1 + if n3 >= 0: + mna._G[n3, n3] += Y1 + if n3 >= 0 and n2 >= 0: + mna._G[n3, n2] -= Y2 + mna._G[n2, n3] -= Y2 + if n3 >= 0: + mna._G[n3, n3] += Y2 + if n2 >= 0: + mna._G[n2, n2] += Y2 class SPpp(Dummy): @@ -2262,9 +2348,6 @@ class W(Dummy): is_wire = True - def _stamp(self, mna): - pass - @property def I(self): raise ValueError( @@ -2322,7 +2405,7 @@ def make(classname, parent, namespace, name, cpt_type, cpt_id, # Dynamically create classes. -defcpt('A', Misc, 'Annotation') +defcpt('A', Ignore, 'Annotation') defcpt('ADC', Misc, 'ADC') defcpt('ANT', Misc, 'Antenna') diff --git a/lcapy/netlistmixin.py b/lcapy/netlistmixin.py index 18ded598..9a0bf286 100644 --- a/lcapy/netlistmixin.py +++ b/lcapy/netlistmixin.py @@ -1057,6 +1057,33 @@ def state_space_model(self): return self.ss_model() + def defs(self, ignore=None): + """Return directory of argname-value pair for all components except + for components with names specified by `ignore`.""" + + defs = {} + for cpt in self._elements.values(): + defs = {**defs, **cpt.defs()} + return defs + + def sympify(self, ignore=None): + """Convert numerical arguments to symbolic arguments except for + components with names specified by `ignore`.""" + + if ignore is None: + ignore = [] + + new = self._new() + + for cpt in self._elements.values(): + + if cpt.name in ignore: + net = cpt._copy() + else: + net = cpt.sympify() + new._add(net) + return new + def ac_model(self, var=omega): """"Create AC model for specified angular frequency (default omega).""" diff --git a/lcapy/schematic.py b/lcapy/schematic.py index 414d0470..1a9df500 100644 --- a/lcapy/schematic.py +++ b/lcapy/schematic.py @@ -599,7 +599,6 @@ def draw(self, filename=None, **kwargs): if key != 'thickness': # val is a str kwargs[key] = val - break def in_ipynb(): try: diff --git a/lcapy/schematics/components/cpt.py b/lcapy/schematics/components/cpt.py index 2407d82b..3952c3ad 100644 --- a/lcapy/schematics/components/cpt.py +++ b/lcapy/schematics/components/cpt.py @@ -50,6 +50,8 @@ def angle_choose(pinpos): class Cpt(object): + # Perhaps use ABC? + voltage_keys = ('v', 'v_', 'v^', 'v_>', 'v_<', 'v^>', 'v^<', 'v<', 'v>') current_keys = ('i', 'i_', 'i^', 'i_>', 'i_<', 'i^>', 'i^<', @@ -62,7 +64,8 @@ class Cpt(object): inner_label_keys = ('t', ) connection_keys = ('input', 'output', 'bidir', 'pad') ground_keys = ('ground', 'sground', 'rground', - 'cground', 'nground', 'pground', '0V') + 'cground', 'nground', 'pground', '0V', + 'tlground', 'tground', 'eground', 'eground2') supply_positive_keys = ('vcc', 'vdd') supply_negative_keys = ('vee', 'vss') supply_keys = supply_positive_keys + supply_negative_keys @@ -111,6 +114,11 @@ class Cpt(object): # node_pinnames maps node numbers to pinnames node_pinnames = () default_pins = () + # pins is a dictionary indexed by pinname. Each entry is a tuple + # (pinpos, x, y) where (x, y) is the pin coordinate. pinpos is a + # string (either 't', 'r', 'b', 'l') indicating where the pin + # label should be placed. If pinpos ends with 'x', the coordinate + # can be scaled. pins = {} # Auxiliary nodes are used for finding the centre of the shape or # to define a bounding box. @@ -336,6 +344,14 @@ def size(self): val = self.default_width return float(val) * self.shape_scale + @property + def width(self): + return self.w * self.size * self.sch.node_spacing + + @property + def height(self): + return self.h * self.size * self.sch.node_spacing + @property def scale(self): return float(self.opts.get('scale', 1.0)) @@ -1130,10 +1146,15 @@ def autoground(self, autoground): raise ValueError('Invalid autoground % s. Choices are % s' % (autoground, ', '.join(self.ground_keys))) + if self.implicit_key(self.opts): + return + for m, node_name in enumerate(self.required_node_names): if node_name != '0': continue + # Check if have implicit ground... + new_node = self.sch.nodes[node_name].split(self) new_node.implicit = True new_node.implicit_symbol = autoground diff --git a/lcapy/schematics/components/shape.py b/lcapy/schematics/components/shape.py index 24653b85..742cd2b2 100644 --- a/lcapy/schematics/components/shape.py +++ b/lcapy/schematics/components/shape.py @@ -16,14 +16,6 @@ class Shape(FixedCpt): 'tl': ('l', -0.5, 0.5), 'tr': ('r', 0.5, 0.5)} - @property - def width(self): - return self.w * self.size * self.sch.node_spacing - - @property - def height(self): - return self.h * self.size * self.sch.node_spacing - def pinpos_rotate(self, pinpos, angle): """Rotate pinpos by multiple of 90 degrees. pinpos is either 'l', 't', 'r', 'b'.""" diff --git a/lcapy/schematics/components/transistor.py b/lcapy/schematics/components/transistor.py index 8a2f98af..903b33a8 100644 --- a/lcapy/schematics/components/transistor.py +++ b/lcapy/schematics/components/transistor.py @@ -70,14 +70,14 @@ def draw(self, **kwargs): label = self.label_tweak(label, xscale, yscale, self.angle) s = r' \draw (%s) node[%s, %s, xscale=%s, yscale=%s, rotate=%d] (%s) {%s};''\n' % ( - centre, cpt, self.draw_args_str(**kwargs), xscale, yscale, + centre, cpt, self.cpt_args_str(**kwargs), xscale, yscale, self.angle, self.s, label) # Add additional wires. These help to compensate for the # slight differences in sizes of the different transistors. if self.tikz_cpt in ('pnp', 'npn'): s += r' \draw[%s] (%s.C) -- (%s) (%s.B) -- (%s) (%s.E) -- (%s);''\n' % ( - self.draw_args_str(**kwargs), self.s, n1.s, self.s, n2.s, self.s, n3.s) + self.cpt_args_str(**kwargs), self.s, n1.s, self.s, n2.s, self.s, n3.s) else: s += r' \draw (%s.D) -- (%s) (%s.G) -- (%s) (%s.S) -- (%s);''\n' % ( self.s, n1.s, self.s, n2.s, self.s, n3.s) diff --git a/lcapy/schemcpts.py b/lcapy/schemcpts.py index 2acf5009..79db8a35 100644 --- a/lcapy/schemcpts.py +++ b/lcapy/schemcpts.py @@ -718,14 +718,16 @@ class Potentiometer(Bipole): """Potentiometer Np, Nm, No""" # This is not really a bipole but circuitikz treats it as such + # Note, the wiper position is not scaled if the component + # is stretched or scaled. can_stretch = False node_pinnames = ('p', 'n', 'wiper') aliases = {'+': 'p', '-': 'n'} - pins = {'p': ('rx', 0, 0), - 'n': ('rx', 1, 0), - 'wiper': ('lx', 0.5, 0.3)} + pins = {'p': ('rx', -0.5, 0), + 'n': ('rx', 0.5, 0), + 'wiper': ('l', 0.0, 0.3)} class VCS(Bipole): @@ -1396,7 +1398,3 @@ def make(classname, parent, namespace, name, cpt_type, cpt_id, defcpt('m', Bipole, 'Mass', 'mass') defcpt('k', Bipole, 'Spring', 'spring') defcpt('r', Bipole, 'Damper', 'damper') - -# Perhaps AM for ammeter, VM for voltmeter, VR for variable resistor? -# Currently, a variable resistor is supported with the variable -# option. diff --git a/lcapy/tests/test_circuits.py b/lcapy/tests/test_circuits.py index 8d5d0b9a..e457dedb 100644 --- a/lcapy/tests/test_circuits.py +++ b/lcapy/tests/test_circuits.py @@ -718,6 +718,25 @@ def test_netlist_subs(self): self.assertEqual(b.V1.Voc, expr(f), "netlist subs") + def test_netlist_sympify(self): + """Test netlist sympify/defs/subs""" + + a = Circuit(''' + R 1 2 1 + C1 2 3 2 + C2 3 4 3 4 + L1 4 5 6 + L2 7 8 9 10''') + + b = a.sympify() + defs = a.defs() + c = b.subs(defs) + + self.assertEqual(a.C1.args, c.C1.args, "C1") + self.assertEqual(a.C2.args, c.C2.args, "C2") + self.assertEqual(a.L1.args, c.L1.args, "L1") + self.assertEqual(a.L2.args, c.L2.args, "L2") + def test_cpt(self): """Test cpt"""