Skip to content

Commit f5735a2

Browse files
committed
Add TransmissionLine block with delay-based scattering model
1 parent f7bd4bb commit f5735a2

3 files changed

Lines changed: 272 additions & 1 deletion

File tree

src/pathsim_rf/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@
1212

1313
__all__ = ["__version__"]
1414

15-
from .network import *
15+
from .transmission_line import *
16+
17+
try:
18+
from .network import *
19+
except ImportError:
20+
pass
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#########################################################################################
2+
##
3+
## Transmission Line Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
import numpy as np
10+
11+
from pathsim.blocks._block import Block
12+
from pathsim.utils.adaptivebuffer import AdaptiveBuffer
13+
14+
# CONSTANTS =============================================================================
15+
16+
C0 = 299792458.0 # speed of light [m/s]
17+
18+
# BLOCKS ================================================================================
19+
20+
class TransmissionLine(Block):
21+
"""Lossy transmission line modeled as a delayed scattering two-port.
22+
23+
In the scattering (wave) domain, the transmission line crosses incident
24+
waves from one port to the other with a propagation delay and attenuation:
25+
26+
.. math::
27+
28+
b_1(t) = T \\cdot a_2(t - \\tau)
29+
30+
.. math::
31+
32+
b_2(t) = T \\cdot a_1(t - \\tau)
33+
34+
where :math:`\\tau = L / v_p` is the one-way propagation delay,
35+
:math:`v_p = c_0 / \\sqrt{\\varepsilon_r}` is the phase velocity,
36+
and :math:`T = 10^{-\\alpha L / 20}` is the voltage transmission
37+
coefficient for attenuation :math:`\\alpha` in dB/m.
38+
39+
The block uses a single vector-valued adaptive interpolating buffer
40+
to delay both wave directions simultaneously.
41+
42+
Parameters
43+
----------
44+
length : float
45+
Physical length of the line [m].
46+
er : float
47+
Effective relative permittivity [-]. Default 1.0 (free space).
48+
attenuation : float
49+
Attenuation constant [dB/m]. Default 0.0 (lossless).
50+
Z0 : float
51+
Characteristic impedance [Ohm]. Stored for reference, does not
52+
affect the scattering computation (matched-line assumption).
53+
"""
54+
55+
input_port_labels = {
56+
"a1": 0,
57+
"a2": 1,
58+
}
59+
60+
output_port_labels = {
61+
"b1": 0,
62+
"b2": 1,
63+
}
64+
65+
def __init__(self, length=1.0, er=1.0, attenuation=0.0, Z0=50.0):
66+
67+
super().__init__()
68+
69+
# input validation
70+
if length <= 0:
71+
raise ValueError(f"'length' must be positive but is {length}")
72+
if er <= 0:
73+
raise ValueError(f"'er' must be positive but is {er}")
74+
if attenuation < 0:
75+
raise ValueError(f"'attenuation' must be non-negative but is {attenuation}")
76+
77+
# store parameters
78+
self.length = length
79+
self.er = er
80+
self.attenuation = attenuation
81+
self.Z0 = Z0
82+
83+
# derived quantities
84+
self.vp = C0 / np.sqrt(er)
85+
self.tau = length / self.vp
86+
self.T = 10.0 ** (-attenuation * length / 20.0)
87+
88+
# single vector-valued buffer for [a1, a2]
89+
self._buffer = AdaptiveBuffer(self.tau)
90+
91+
def __len__(self):
92+
# no algebraic passthrough — output depends on past input only
93+
return 0
94+
95+
def reset(self):
96+
super().reset()
97+
self._buffer.clear()
98+
99+
def sample(self, t, dt):
100+
"""Store current incident waves into the delay buffer."""
101+
self._buffer.add(t, np.array([self.inputs[0], self.inputs[1]]))
102+
103+
def update(self, t):
104+
"""Read delayed waves, cross and scale."""
105+
delayed = self._buffer.get(t)
106+
107+
if np.isscalar(delayed):
108+
# buffer not yet filled (t < tau)
109+
self.outputs[0] = 0.0
110+
self.outputs[1] = 0.0
111+
else:
112+
# b1 = T * a2(t-tau), b2 = T * a1(t-tau)
113+
self.outputs[0] = self.T * delayed[1]
114+
self.outputs[1] = self.T * delayed[0]

tests/test_transmission_line.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
########################################################################################
2+
##
3+
## TESTS FOR
4+
## 'transmission_line.py'
5+
##
6+
########################################################################################
7+
8+
# IMPORTS ==============================================================================
9+
10+
import unittest
11+
import numpy as np
12+
13+
from pathsim_rf import TransmissionLine
14+
from pathsim_rf.transmission_line import C0
15+
16+
17+
# TESTS ================================================================================
18+
19+
class TestTransmissionLine(unittest.TestCase):
20+
"""Test the TransmissionLine block."""
21+
22+
def test_init_default(self):
23+
"""Test default initialization."""
24+
tl = TransmissionLine()
25+
self.assertEqual(tl.length, 1.0)
26+
self.assertEqual(tl.er, 1.0)
27+
self.assertEqual(tl.attenuation, 0.0)
28+
self.assertEqual(tl.Z0, 50.0)
29+
30+
def test_init_custom(self):
31+
"""Test custom initialization."""
32+
tl = TransmissionLine(length=0.5, er=4.0, attenuation=0.1, Z0=75.0)
33+
self.assertEqual(tl.length, 0.5)
34+
self.assertEqual(tl.er, 4.0)
35+
self.assertEqual(tl.attenuation, 0.1)
36+
self.assertEqual(tl.Z0, 75.0)
37+
38+
def test_derived_quantities(self):
39+
"""Verify propagation velocity, delay, and transmission coefficient."""
40+
tl = TransmissionLine(length=2.0, er=4.0, attenuation=0.5)
41+
42+
expected_vp = C0 / np.sqrt(4.0)
43+
expected_tau = 2.0 / expected_vp
44+
expected_T = 10.0 ** (-0.5 * 2.0 / 20.0)
45+
46+
self.assertAlmostEqual(tl.vp, expected_vp)
47+
self.assertAlmostEqual(tl.tau, expected_tau)
48+
self.assertAlmostEqual(tl.T, expected_T)
49+
50+
def test_lossless_transmission(self):
51+
"""Lossless line has T = 1."""
52+
tl = TransmissionLine(attenuation=0.0)
53+
self.assertAlmostEqual(tl.T, 1.0)
54+
55+
def test_init_validation(self):
56+
"""Test input validation."""
57+
with self.assertRaises(ValueError):
58+
TransmissionLine(length=0)
59+
with self.assertRaises(ValueError):
60+
TransmissionLine(length=-1)
61+
with self.assertRaises(ValueError):
62+
TransmissionLine(er=0)
63+
with self.assertRaises(ValueError):
64+
TransmissionLine(attenuation=-0.1)
65+
66+
def test_port_labels(self):
67+
"""Test port label definitions."""
68+
self.assertEqual(TransmissionLine.input_port_labels["a1"], 0)
69+
self.assertEqual(TransmissionLine.input_port_labels["a2"], 1)
70+
self.assertEqual(TransmissionLine.output_port_labels["b1"], 0)
71+
self.assertEqual(TransmissionLine.output_port_labels["b2"], 1)
72+
73+
def test_no_passthrough(self):
74+
"""Delay block has no algebraic passthrough."""
75+
tl = TransmissionLine()
76+
self.assertEqual(len(tl), 0)
77+
78+
def test_output_zero_before_delay(self):
79+
"""Before the buffer fills, output should be zero."""
80+
tl = TransmissionLine(length=1.0, er=1.0)
81+
82+
tl.inputs[0] = 1.0
83+
tl.inputs[1] = 2.0
84+
tl.sample(0.0, 0.01)
85+
tl.update(0.0)
86+
87+
self.assertAlmostEqual(tl.outputs[0], 0.0)
88+
self.assertAlmostEqual(tl.outputs[1], 0.0)
89+
90+
def test_crossing(self):
91+
"""Verify that a1 appears at b2 and a2 appears at b1 after delay."""
92+
tau = 1e-9 # short line for easy testing
93+
length = tau * C0 # er=1
94+
95+
tl = TransmissionLine(length=length, er=1.0, attenuation=0.0)
96+
self.assertAlmostEqual(tl.tau, tau)
97+
98+
# Fill buffer with constant input over several samples
99+
dt = tau / 10
100+
for i in range(20):
101+
t = i * dt
102+
tl.inputs[0] = 3.0 # a1
103+
tl.inputs[1] = 7.0 # a2
104+
tl.sample(t, dt)
105+
106+
# Query at t > tau — should see crossed, unattenuated output
107+
t_query = 15 * dt
108+
tl.update(t_query)
109+
110+
self.assertAlmostEqual(tl.outputs[0], 7.0, places=1) # b1 = T * a2
111+
self.assertAlmostEqual(tl.outputs[1], 3.0, places=1) # b2 = T * a1
112+
113+
def test_attenuation(self):
114+
"""Verify that attenuation scales the output correctly."""
115+
tau = 1e-9
116+
length = tau * C0
117+
atten_dB_per_m = 3.0 # 3 dB/m
118+
119+
tl = TransmissionLine(length=length, er=1.0, attenuation=atten_dB_per_m)
120+
expected_T = 10.0 ** (-atten_dB_per_m * length / 20.0)
121+
122+
# Fill buffer
123+
dt = tau / 10
124+
for i in range(20):
125+
t = i * dt
126+
tl.inputs[0] = 1.0
127+
tl.inputs[1] = 1.0
128+
tl.sample(t, dt)
129+
130+
tl.update(15 * dt)
131+
132+
self.assertAlmostEqual(tl.outputs[0], expected_T, places=1)
133+
self.assertAlmostEqual(tl.outputs[1], expected_T, places=1)
134+
135+
def test_reset(self):
136+
"""After reset, buffer should be empty and outputs zero."""
137+
tl = TransmissionLine()
138+
tl.inputs[0] = 5.0
139+
tl.inputs[1] = 5.0
140+
tl.sample(0.0, 0.01)
141+
142+
tl.reset()
143+
tl.update(1.0)
144+
145+
self.assertAlmostEqual(tl.outputs[0], 0.0)
146+
self.assertAlmostEqual(tl.outputs[1], 0.0)
147+
148+
149+
# RUN TESTS LOCALLY ====================================================================
150+
151+
if __name__ == '__main__':
152+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)