|
| 1 | +######################################################################################## |
| 2 | +## |
| 3 | +## Testing logic and comparison block systems |
| 4 | +## |
| 5 | +## Verifies comparison (GreaterThan, LessThan, Equal) and boolean logic |
| 6 | +## (LogicAnd, LogicOr, LogicNot) blocks in full simulation context. |
| 7 | +## |
| 8 | +######################################################################################## |
| 9 | + |
| 10 | +# IMPORTS ============================================================================== |
| 11 | + |
| 12 | +import unittest |
| 13 | +import numpy as np |
| 14 | + |
| 15 | +from pathsim import Simulation, Connection |
| 16 | +from pathsim.blocks import ( |
| 17 | + Source, |
| 18 | + Constant, |
| 19 | + Scope, |
| 20 | + ) |
| 21 | + |
| 22 | +from pathsim.blocks.logic import ( |
| 23 | + GreaterThan, |
| 24 | + LessThan, |
| 25 | + Equal, |
| 26 | + LogicAnd, |
| 27 | + LogicOr, |
| 28 | + LogicNot, |
| 29 | + ) |
| 30 | + |
| 31 | + |
| 32 | +# TESTCASE ============================================================================= |
| 33 | + |
| 34 | +class TestComparisonSystem(unittest.TestCase): |
| 35 | + """ |
| 36 | + Test comparison blocks in a simulation that compares a sine wave |
| 37 | + against a constant threshold. |
| 38 | +
|
| 39 | + System: Source(sin(t)) → GT/LT/EQ → Scope |
| 40 | + Constant(0) ↗ |
| 41 | +
|
| 42 | + Verify: GT outputs 1 when sin(t) > 0, LT outputs 1 when sin(t) < 0 |
| 43 | + """ |
| 44 | + |
| 45 | + def setUp(self): |
| 46 | + |
| 47 | + Src = Source(lambda t: np.sin(2 * np.pi * t)) |
| 48 | + Thr = Constant(0.0) |
| 49 | + |
| 50 | + self.GT = GreaterThan() |
| 51 | + self.LT = LessThan() |
| 52 | + |
| 53 | + self.Sco = Scope(labels=["signal", "gt_zero", "lt_zero"]) |
| 54 | + |
| 55 | + blocks = [Src, Thr, self.GT, self.LT, self.Sco] |
| 56 | + |
| 57 | + connections = [ |
| 58 | + Connection(Src, self.GT["a"], self.LT["a"], self.Sco[0]), |
| 59 | + Connection(Thr, self.GT["b"], self.LT["b"]), |
| 60 | + Connection(self.GT, self.Sco[1]), |
| 61 | + Connection(self.LT, self.Sco[2]), |
| 62 | + ] |
| 63 | + |
| 64 | + self.Sim = Simulation( |
| 65 | + blocks, |
| 66 | + connections, |
| 67 | + dt=0.01, |
| 68 | + log=False |
| 69 | + ) |
| 70 | + |
| 71 | + |
| 72 | + def test_gt_lt_complementary(self): |
| 73 | + """GT and LT should be complementary (sum to 1) away from zero crossings""" |
| 74 | + |
| 75 | + self.Sim.run(duration=3.0, reset=True) |
| 76 | + |
| 77 | + time, [sig, gt, lt] = self.Sco.read() |
| 78 | + |
| 79 | + #away from zero crossings, GT + LT should be 1 (exactly one is true) |
| 80 | + mask = np.abs(sig) > 0.1 |
| 81 | + result = gt[mask] + lt[mask] |
| 82 | + |
| 83 | + self.assertTrue(np.allclose(result, 1.0), |
| 84 | + "GT and LT should be complementary away from zero crossings") |
| 85 | + |
| 86 | + |
| 87 | + def test_gt_matches_positive(self): |
| 88 | + """GT output should be 1 when signal is clearly positive""" |
| 89 | + |
| 90 | + self.Sim.run(duration=3.0, reset=True) |
| 91 | + |
| 92 | + time, [sig, gt, lt] = self.Sco.read() |
| 93 | + |
| 94 | + mask_pos = sig > 0.2 |
| 95 | + self.assertTrue(np.all(gt[mask_pos] == 1.0), |
| 96 | + "GT should be 1 when signal is positive") |
| 97 | + |
| 98 | + mask_neg = sig < -0.2 |
| 99 | + self.assertTrue(np.all(gt[mask_neg] == 0.0), |
| 100 | + "GT should be 0 when signal is negative") |
| 101 | + |
| 102 | + |
| 103 | +class TestLogicGateSystem(unittest.TestCase): |
| 104 | + """ |
| 105 | + Test logic gates combining two comparison outputs. |
| 106 | +
|
| 107 | + System: Two sine waves at different frequencies compared against 0, |
| 108 | + then combined with AND/OR/NOT. |
| 109 | +
|
| 110 | + Verify: Logic truth tables hold across the simulation. |
| 111 | + """ |
| 112 | + |
| 113 | + def setUp(self): |
| 114 | + |
| 115 | + #two signals with different frequencies so they go in and out of phase |
| 116 | + Src1 = Source(lambda t: np.sin(2 * np.pi * 1.0 * t)) |
| 117 | + Src2 = Source(lambda t: np.sin(2 * np.pi * 1.5 * t)) |
| 118 | + Zero = Constant(0.0) |
| 119 | + |
| 120 | + GT1 = GreaterThan() |
| 121 | + GT2 = GreaterThan() |
| 122 | + |
| 123 | + self.AND = LogicAnd() |
| 124 | + self.OR = LogicOr() |
| 125 | + self.NOT = LogicNot() |
| 126 | + |
| 127 | + self.Sco = Scope(labels=["gt1", "gt2", "and", "or", "not1"]) |
| 128 | + |
| 129 | + blocks = [Src1, Src2, Zero, GT1, GT2, |
| 130 | + self.AND, self.OR, self.NOT, self.Sco] |
| 131 | + |
| 132 | + connections = [ |
| 133 | + Connection(Src1, GT1["a"]), |
| 134 | + Connection(Src2, GT2["a"]), |
| 135 | + Connection(Zero, GT1["b"], GT2["b"]), |
| 136 | + Connection(GT1, self.AND["a"], self.OR["a"], self.NOT, self.Sco[0]), |
| 137 | + Connection(GT2, self.AND["b"], self.OR["b"], self.Sco[1]), |
| 138 | + Connection(self.AND, self.Sco[2]), |
| 139 | + Connection(self.OR, self.Sco[3]), |
| 140 | + Connection(self.NOT, self.Sco[4]), |
| 141 | + ] |
| 142 | + |
| 143 | + self.Sim = Simulation( |
| 144 | + blocks, |
| 145 | + connections, |
| 146 | + dt=0.01, |
| 147 | + log=False |
| 148 | + ) |
| 149 | + |
| 150 | + |
| 151 | + def test_and_gate(self): |
| 152 | + """AND should only be 1 when both inputs are 1""" |
| 153 | + |
| 154 | + self.Sim.run(duration=5.0, reset=True) |
| 155 | + |
| 156 | + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() |
| 157 | + |
| 158 | + #where both are 1, AND should be 1 |
| 159 | + both_true = (gt1 == 1.0) & (gt2 == 1.0) |
| 160 | + if np.any(both_true): |
| 161 | + self.assertTrue(np.all(and_out[both_true] == 1.0)) |
| 162 | + |
| 163 | + #where either is 0, AND should be 0 |
| 164 | + either_false = (gt1 == 0.0) | (gt2 == 0.0) |
| 165 | + if np.any(either_false): |
| 166 | + self.assertTrue(np.all(and_out[either_false] == 0.0)) |
| 167 | + |
| 168 | + |
| 169 | + def test_or_gate(self): |
| 170 | + """OR should be 1 when either input is 1""" |
| 171 | + |
| 172 | + self.Sim.run(duration=5.0, reset=True) |
| 173 | + |
| 174 | + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() |
| 175 | + |
| 176 | + #where both are 0, OR should be 0 |
| 177 | + both_false = (gt1 == 0.0) & (gt2 == 0.0) |
| 178 | + if np.any(both_false): |
| 179 | + self.assertTrue(np.all(or_out[both_false] == 0.0)) |
| 180 | + |
| 181 | + #where either is 1, OR should be 1 |
| 182 | + either_true = (gt1 == 1.0) | (gt2 == 1.0) |
| 183 | + if np.any(either_true): |
| 184 | + self.assertTrue(np.all(or_out[either_true] == 1.0)) |
| 185 | + |
| 186 | + |
| 187 | + def test_not_gate(self): |
| 188 | + """NOT should invert its input""" |
| 189 | + |
| 190 | + self.Sim.run(duration=5.0, reset=True) |
| 191 | + |
| 192 | + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() |
| 193 | + |
| 194 | + #NOT should be inverse of GT1 |
| 195 | + self.assertTrue(np.allclose(not_out + gt1, 1.0), |
| 196 | + "NOT should invert its input") |
| 197 | + |
| 198 | + |
| 199 | +class TestEqualSystem(unittest.TestCase): |
| 200 | + """ |
| 201 | + Test Equal block detecting when two signals are close. |
| 202 | +
|
| 203 | + System: Source(sin(t)) → Equal ← Source(sin(t + small_offset)) |
| 204 | + """ |
| 205 | + |
| 206 | + def test_equal_detects_match(self): |
| 207 | + """Equal should output 1 when signals match within tolerance""" |
| 208 | + |
| 209 | + Src1 = Constant(3.14) |
| 210 | + Src2 = Constant(3.14) |
| 211 | + |
| 212 | + Eq = Equal(tolerance=0.01) |
| 213 | + Sco = Scope() |
| 214 | + |
| 215 | + Sim = Simulation( |
| 216 | + blocks=[Src1, Src2, Eq, Sco], |
| 217 | + connections=[ |
| 218 | + Connection(Src1, Eq["a"]), |
| 219 | + Connection(Src2, Eq["b"]), |
| 220 | + Connection(Eq, Sco), |
| 221 | + ], |
| 222 | + dt=0.1, |
| 223 | + log=False |
| 224 | + ) |
| 225 | + |
| 226 | + Sim.run(duration=1.0, reset=True) |
| 227 | + |
| 228 | + time, [eq_out] = Sco.read() |
| 229 | + |
| 230 | + self.assertTrue(np.all(eq_out == 1.0), |
| 231 | + "Equal should output 1 for identical signals") |
| 232 | + |
| 233 | + |
| 234 | + def test_equal_detects_mismatch(self): |
| 235 | + """Equal should output 0 when signals differ""" |
| 236 | + |
| 237 | + Src1 = Constant(1.0) |
| 238 | + Src2 = Constant(2.0) |
| 239 | + |
| 240 | + Eq = Equal(tolerance=0.01) |
| 241 | + Sco = Scope() |
| 242 | + |
| 243 | + Sim = Simulation( |
| 244 | + blocks=[Src1, Src2, Eq, Sco], |
| 245 | + connections=[ |
| 246 | + Connection(Src1, Eq["a"]), |
| 247 | + Connection(Src2, Eq["b"]), |
| 248 | + Connection(Eq, Sco), |
| 249 | + ], |
| 250 | + dt=0.1, |
| 251 | + log=False |
| 252 | + ) |
| 253 | + |
| 254 | + Sim.run(duration=1.0, reset=True) |
| 255 | + |
| 256 | + time, [eq_out] = Sco.read() |
| 257 | + |
| 258 | + self.assertTrue(np.all(eq_out == 0.0), |
| 259 | + "Equal should output 0 for different signals") |
| 260 | + |
| 261 | + |
| 262 | +# RUN TESTS LOCALLY ==================================================================== |
| 263 | + |
| 264 | +if __name__ == '__main__': |
| 265 | + unittest.main(verbosity=2) |
0 commit comments