Sensor Daisy Chain#

This tutorial will analyze a string of sensors wired as a daisy chain where the string is powered from one end. This configuration is common in e.g. instrumentation of production lines, seismic acquisitions systems etc.

The system consists of the following components:

  • A main acquisition unit, powered from a 14.4V Li-ion battery. A voltage booster powers the daisy chain with 48V.

  • 16 sensor units, spaced 16m apart and connected with twisted pair wire for power. The sensor units consume 0.35W from 3.3V. The power input is polarity protected with a diode. A power switch enables power to the next unit.

from sysloss.components import *
from sysloss.system import System
import pandas as pd
import matplotlib.pyplot as plt

System definition#

The system is defined using the System class.

Tip

When you want to analyze a system with variations on specific parameters, it can be very effective to define the system in a function, with target parameters as function arguments.

Since we have 16 identical units, it is practical to use a loop to add the components. Wire resistance depends on wire gauge, and we define that as a parameter to the create_sys() function below.

Note

Component names must be unique in the system. Use e.g. an index added to the name.

def create_sys(wire_res=4.3, vdrop=0.54, eff=0.72):
    sdc = System("Sensor Daisy chain", Source("Li-ion", vo=14.4, rs=0.2))
    sdc.add_comp("Li-ion", comp=Converter("Boost 48V", vo=48.0, eff=.88))
    parent = "Boost 48V"
    for i in range(1,17,1):
        idx = " [{}]".format(i)
        sdc.add_comp(parent, comp=RLoss("Twisted pair"+idx, rs=wire_res))
        sdc.add_comp("Twisted pair"+idx, comp=VLoss("Diode"+idx, vdrop=vdrop))
        sdc.add_comp("Diode"+idx, comp=PSwitch("Power switch"+idx, rs=0.03, ig=10e-6))
        sdc.add_comp("Diode"+idx, comp=Converter("Buck 3.3V"+idx, vo=3.3, eff=eff))
        sdc.add_comp("Buck 3.3V"+idx, comp=PLoad("Sensor unit"+idx, pwr=0.35))
        parent = "Power switch"+idx
    return sdc

This is a deep power tree (~50 levels), so let’s look at the last few nodes only:

sdc = create_sys()
sdc.tree("Twisted pair [12]")
Sensor Daisy chain
└── Twisted pair [12]
    └── Diode [12]
        ├── Buck 3.3V [12]
        │   └── Sensor unit [12]
        └── Power switch [12]
            └── Twisted pair [13]
                └── Diode [13]
                    ├── Buck 3.3V [13]
                    │   └── Sensor unit [13]
                    └── Power switch [13]
                        └── Twisted pair [14]
                            └── Diode [14]
                                ├── Buck 3.3V [14]
                                │   └── Sensor unit [14]
                                └── Power switch [14]
                                    └── Twisted pair [15]
                                        └── Diode [15]
                                            ├── Buck 3.3V [15]
                                            │   └── Sensor unit [15]
                                            └── Power switch [15]
                                                └── Twisted pair [16]
                                                    └── Diode [16]
                                                        ├── Buck 3.3V [16]
                                                        │   └── Sensor unit [16]
                                                        └── Power switch [16]

Analysis#

In the analysis phase we will check the power efficiency of the daisy chain with different wire gauges. Note that current runs both directions on the power pair, so effective resistance is twice that of a single wire.

Wire gauge

Resistance (ohm/m)

20

0.0333

22

0.053

24

0.084

26

0.134

28

0.213

Tip

Use the tags argument of the .solve() function to tag the results table with custom columns.

wlen = 16 * 2 # total wire length between sensor nodes
wirelist = {"AWG20": wlen*0.0333, "AWG22": wlen*0.053, "AWG24": wlen*0.084, "AWG26": wlen*0.134, "AWG28": wlen*0.213}
res = []
for wire in wirelist.keys():
    sdc = create_sys(wire_res=wirelist[wire])
    res += [sdc.solve(tags={"Wire":wire})]
df = pd.concat(res, ignore_index=True)
df
Component Type Parent Wire Vin (V) Vout (V) Iin (A) Iout (A) Power (W) Loss (W) Efficiency (%) Warnings
0 Li-ion SOURCE AWG20 14.4 14.258142 0.709291 0.709291 10.213786 0.100619 99.014874
1 Boost 48V CONVERTER Li-ion AWG20 14.258142 48.0 0.709291 0.185408 10.113168 1.21358 88.0
2 Twisted pair [1] SLOSS Boost 48V AWG20 48.0 47.802429 0.185408 0.185408 8.899588 0.036631 99.588394
3 Diode [1] SLOSS Twisted pair [1] AWG20 47.802429 47.262429 0.185408 0.185408 8.862961 0.10012 98.87035
4 Buck 3.3V [1] CONVERTER Diode [1] AWG20 47.262429 3.3 0.010285 0.106061 0.486111 0.136111 72.0
... ... ... ... ... ... ... ... ... ... ... ... ...
410 Diode [16] SLOSS Twisted pair [16] AWG28 23.680869 23.140871 0.021017 0.021017 0.497691 0.011349 97.719678
411 Buck 3.3V [16] CONVERTER Diode [16] AWG28 23.140871 3.3 0.021007 0.106061 0.486111 0.136111 72.0
412 Sensor unit [16] LOAD Buck 3.3V [16] AWG28 3.3 0.0 0.106061 0.0 0.35 0.0 100.0
413 Power switch [16] PSWITCH Diode [16] AWG28 23.140871 23.140871 0.00001 0.0 0.000231 0.000231 0.0
414 System total AWG28 0.960525 13.831559 8.231672 40.486306

415 rows × 12 columns

The results table is now quite large - let’s look at the System total only:

df[df.Component == "System total"][["Component", "Wire", "Power (W)", "Loss (W)", "Efficiency (%)"]].style.hide(axis='index')
Component Wire Power (W) Loss (W) Efficiency (%)
System total AWG20 10.213786 4.613818 54.827546
System total AWG22 10.412506 4.812548 53.781077
System total AWG24 10.768457 5.168515 52.003197
System total AWG26 11.507698 5.907774 48.662414
System total AWG28 13.831559 8.231672 40.486306

From the first table we can see that the output voltage from the last sensor node is 23.16V. Feeling optimistic - will it work with AWG30 also?

sdc = create_sys(wire_res=16*2*0.338)
sdc.solve()

Hide code cell output

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 2
      1 sdc = create_sys(wire_res=16*2*0.338)
----> 2 sdc.solve()

File ~/checkouts/readthedocs.org/user_builds/sysloss/checkouts/latest/src/sysloss/system.py:923, in System.solve(self, vtol, itol, maxiter, quiet, phase, energy, ta, tags)
    921 frames = []
    922 for ph in phase_list:
--> 923     v, i, iters, state = self._solve(vtol, itol, maxiter, quiet, ph)
    924     if iters > maxiter:
    925         raise RuntimeError(
    926             "Steady-state not achieved after {} iterations".format(iters - 1)
    927         )

File ~/checkouts/readthedocs.org/user_builds/sysloss/checkouts/latest/src/sysloss/system.py:813, in System._solve(self, vtol, itol, maxiter, quiet, phase)
    811 iters = 0
    812 while iters <= maxiter:
--> 813     vi, ostate = self._fwd_prop(v, i, phase, state)
    814     ii = self._back_prop(vi, i, phase, state)
    815     iters += 1

File ~/checkouts/readthedocs.org/user_builds/sysloss/checkouts/latest/src/sysloss/system.py:768, in System._fwd_prop(self, v, i, phase, state)
    765     if self._childs[n] != -1:  # not leaf
    766         # sum currents into childs
    767         io = self._child_curr(n, i, v, state)
--> 768     vo[n], ostate[n] = self._g[n]._solv_outp_volt(
    769         vi, ii, io, phase, phase_config, pstate
    770     )
    772 return vo, ostate

File ~/checkouts/readthedocs.org/user_builds/sysloss/checkouts/latest/src/sysloss/components.py:813, in RLoss._solv_outp_volt(self, vi, ii, io, phase, phase_conf, pstate)
    811 if np.sign(vo) == np.sign(vi[0]):
    812     return vo, STATE_DEFAULT
--> 813 raise ValueError(
    814     "Unstable system: RLoss component '{}' has zero output voltage".format(
    815         self._params["name"]
    816     )
    817 )

ValueError: Unstable system: RLoss component 'Twisted pair [14]' has zero output voltage

.solve() now throws an exception: The input voltage to sensor node 14 drops to zero. There is no steady-state solution for this system, so AWG30 is not an option. It would work with 13 sensor nodes, however.

Finally, let’s plot the voltage along the daisy chain for the different wire gauges.

for i in range(0,9,2):
    plt.plot(range(1,17,1), df[(df.Component.str.startswith("Diode")) & (df.Wire=="AWG2"+str(i))][["Vin (V)"]].values, marker='.', label="AWG2"+str(i))
plt.legend()
plt.xlabel("Sensor node #")
plt.ylabel("Input voltage (V)")
plt.grid()
plt.title("Daisy chain voltage distribution");
../_images/cfa3a03334c182356266d1f95c74e502c35857d3b7b273501cee83f593adf913.png

Improve accuracy with parameter interpolation#

Both current and voltage drop significantly along the daisy chain. The input voltage affects the 3.3V buck converter efficiency, and the current affects the diode forward voltage drop. Next, we define interpolation data for the diode voltage drop and converter efficiency, both as a factor of output current. The data points are extracted from the component’s data sheet.

diode_vdrop = {"vi": [48], "io":[1e-3, 1e-2, 0.1, 1, 2, 3, 5], "vdrop":[[.25, .325, 0.4, 0.65, 0.8, 0.9, 1.1]]}
buck_eff = {"vi": [48, 36, 24, 12], "io": [1e-3, 10e-3, 0.1, 0.2, 0.3], 
            "eff":[[0.61,0.63,0.66,0.68,0.69],[0.63,0.65,0.68,0.705,0.72],[0.66,.68,0.72,0.73,0.74],[0.7,0.72,0.76,0.78,0.77]]}
res = []
for wire in wirelist.keys():
    sdc = create_sys(wire_res=wirelist[wire], vdrop=diode_vdrop, eff=buck_eff)
    res += [sdc.solve(tags={"Wire":wire})]
df = pd.concat(res, ignore_index=True)
df[df.Component == "System total"][["Component", "Wire", "Power (W)", "Loss (W)", "Efficiency (%)"]].style.hide(axis='index')
Component Wire Power (W) Loss (W) Efficiency (%)
System total AWG20 10.686804 5.086870 52.400460
System total AWG22 10.872031 5.272123 51.507468
System total AWG24 11.201731 5.601822 49.991459
System total AWG26 11.853800 6.253918 47.241240
System total AWG28 13.545782 7.945931 41.340182

Let’s look at the converter efficiency and diode power loss as a function of wire gauge:

for i in range(0,9,2):
    plt.plot(range(1,17,1), df[(df.Component.str.startswith("Buck")) & (df.Wire=="AWG2"+str(i))][["Efficiency (%)"]].values, marker='.', label="AWG2"+str(i))
plt.legend()
plt.xlabel("Sensor node #")
plt.ylabel("Efficiency (%)")
plt.grid()
plt.title("3.3V buck converter efficiency");
../_images/1790216d06913547d64679d869f70cf14d71de618b20e3c69c0ae39654571530.png
for i in range(0,9,2):
    plt.plot(range(1,17,1), df[(df.Component.str.startswith("Diode")) & (df.Wire=="AWG2"+str(i))][["Loss (W)"]].values, marker='.', label="AWG2"+str(i))
plt.legend()
plt.xlabel("Sensor node #")
plt.ylabel("Loss (W)")
plt.grid()
plt.title("Diode power loss");
../_images/cd17036f8d3703962bce9d71ed6160579c6e646fac5643c18aac24cfe642afcc.png

Tip

Double-check interpolation data by using the plot_interp() method. 2D data can be plotted as either a 2D color map or 3D surface, with or without input data marked. Interpolation outside of the input data points is using the last valid data point as value.

sdc.plot_interp("Buck 3.3V [1]");
../_images/aa366fac98c2be4c0d19418808d615c8dfce6f9cd8378cf5c41926ad2518444d.png
sdc.plot_interp("Buck 3.3V [1]", inpdata=False, plot3d=True);
../_images/355fb7494e2fc0dc5405868e5d414ab4294ae4c15a743956fe7645760b6af9e5.png
sdc.plot_interp("Diode [1]");
../_images/4efc158041512dc89b06a3e6ba16abc74a167f636ff31ccbeeceeda8be79a11e.png

Summary#

This notebook demonstrates how defining a system creation function helps explore system parameters. Further on 2D parameter interpolation is used to define converter efficiency as a function of both input voltage and output current.