|
| 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