mph-/lcapy

Signs on branch voltages and currents break Tellegen's theorem

timchinowsky opened this issue · 16 comments

Looking at branch currents and voltages with an eye to demonstrating Tellegen's theorem, I'm seeing that the sign conventions for the branch voltages V and currents I (at least the way I'm accessing them) do not agree with the "associated reference directions" convention which enables concise expression of Tellegen's theorem as I(transpose)*V = 0. I've attach a zipped Jupyter notebook demonstrating the issue, and how it resolves if I manually change the signs to conform.

tellegen.zip

Screenshot from 2023-11-04 15-57-44
Screenshot from 2023-11-04 15-45-27

mph- commented

Lcapy uses the passive sign convention. It looks like for circuit graph theory the source currents need to be negated. Say, something like:

branch_cpts = [cct[branch_name] for branch_name in cct.branch_list]
currents = [-cpt.I if cpt.is_source else cpt.I for cpt in branch_cpts]

I welcome any suggestions to provide this functionality in a concise manner.

mph- commented

Yikes, on reflection, it seems like a hybrid sign convention is used since this was more intuitive. This is going to take a lot of pondering to resolve.

Thought it might be a bit of a can of worms... the way Chua does it is so clean, love it, just discovered that recently.
His approach makes plain that the circuit graph is separate in many ways from the circuit elements -- for instance Tellegen's theorem holds for ANY set of voltages satisfying KCL and ANY set of voltages satisfying KVL in a given circuit graph - even if the voltages result from different circuit elements than the currents. With this approach, any sign conventions which try to follow rules other than that based on the circuit graph are doomed to fail. So I'd say do something like sort the nodes by name, then whenever current goes from a node which is lower in the sort order to one which is higher in the sort order, that's a positive current, and correspondingly for branch voltages node_i is positive relative to node_j if j>i. That way if you have the same node names and branches you will end up with the same graph, same incidence matrix, etc and Tellegen's theorem will naturally continue to hold even if you change the elements which are on each branch.

Or you could let the user provide their own list of node names, where the first one is the reference node, and the order is defined by the list.

mph- commented

I've created a branch (psc) that fixes your problem by enforcing the passive sign convention.

That was the easy part and I think it makes good sense.

The tricky part is rolling this change out in a graceful manner and updating the docs.

Great, makes sense... I guess the other part is how the program assigns which side is positive and which is negative, which is an arbitrary distinction as far as the theory goes but different choices might be more or less convenient in practice. Which docs are you thinking of in particular?

mph- commented

When a component is defined in a netlist, the first node is considered to be positive and the second is considered negative.

The tricky thing for a user is something like the following:

>>> a = Circuit("""
    I1 1 0
    R 1 0""")
>>> a.I1.I
-I₁

Try this one, it's still giving me a non-zero product. However if I change Cin to Rin it gives zero...

from lcapy import Circuit
import sympy

# make vectors of branch voltages and currents (clumsily)

def v_branches(c):
    v = sympy.Matrix([list(c[b].V.expr.values())[0] for b in c.branch_list])
    v.simplify()
    return v

def i_branches(c):
    i = sympy.Matrix([list(c[b].I.expr.values())[0] for b in c.branch_list])
    i.simplify()
    return i

d=Circuit("""
Vi 1 0 step; down
R 1 2; right 
Cin 2 0_2; down, v=v_{in}
E1 3 0_3 2 0 A; down, l=A v_{in}
Vout 3 4; right
RL 4 0_4; down
W 0 0_2
W 0_2 0_3;
W 0_3 0_4
""")

d.draw()
ii = i_branches(d)
display(ii)
vv = v_branches(d)
display(vv)
prod = (ii.transpose()*vv)
prod.simplify()
prod
mph- commented

You were ignoring the DC component.

It works if you convert to the time domain.

 def v_branches(c):
     v = sympy.Matrix([c[b].v.expr for b in c.branch_list])
     v.simplify()
     return v


 def i_branches(c):
     i = sympy.Matrix([c[b].i.expr for b in c.branch_list])
     i.simplify()
    return i
mph- commented

p.s. you can also do this more succinctly with

from lcapy import Circuit, Matrix

def v_branches(c):
    v = Matrix([c[b].v for b in c.branch_list])
    v.simplify()
   return v

or better still with the Vector class.

Mmm thanks for the branch voltage and current functions!
Figuring out ignoring the DC component, I'm wrestling with domains... I'm seeing that for instance

from lcapy import Circuit

d=Circuit("""
Vi 1 0 ac; down
R 1 2; right 
Cin 2 0_2; down, v=v_{in}
E1 3 0_3 2 0 A; down, l=A v_{in}
Vout 3 4 ac; right
RL 4 0_4; down
W 0 0_2
W 0_2 0_3;
W 0_3 0_4
""")

d.draw()
d.describe()
display(d.branch_list)
bc = d.branch_currents()
display(bc)
bv = d.branch_voltages()
display(bv)
prod = (bc.transpose()*bv)
prod.simplify()

works in the phasor domain, I get expressions which seem reasonable, with lots of cos(w0t)'s, and branch voltages and currents which dot product to 0. How do I do the same in the s domain? I'd expect to be able to get branch voltages and currents as a function of s, but haven't been able to.

mph- commented

Okay, I see the problem. The branch_currents() and branch_voltages() methods are using the netlist i and v attributes. These convert the expressions to the time domain. Instead, they should us the I an V attributes.

I'll push a patch after I've marked another pile of exam scripts :-)

mph- commented

I've modified the branch_currents and branch_voltages methods to return an ExprList of SuperpositionCurrent or SuperpositionVoltage elements.

Unfortunately, SymPy requires Matrix objects to only contain SymPy expressions (it used to be more flexible). To get a vector of time-domain currents:

 Vector(cct.branch_currents()(t))

Similarly for a vector of Laplace-domain currents:

Vector(cct.branch_currents()(s))

If you are keen you can also extract the DC, AC, transient, and noise components although I'm not particularly keen on the user interface for this.

Thanks! I think you snipped something important out of branch_voltages() by mistake, it's returning None. Also, after I fix that, the LaTex printer seems to barf on this example in Jupyter:

d = Circuit("""
Vi 1 0 {V(s)}; down
R1 1 2; right 
R2 2 0_2; down
W 0 0_2
""")

d.draw()
d.describe()
display(Vector(d.branch_list))
bv = Vector(d.branch_voltages()(s)).simplify()
display(bv)
bc = Vector(d.branch_currents()(s)).simplify()
display(bc)
prod = bv.transpose()*bc
prod.simplify()
prod
mph- commented

Hopefully both issues are fixed. I should not push patches after marking! Anyway, I've added a unit test that I should have done yesterday.

So far so good...