Skip to content

Commit f02e3cb

Browse files
committed
Add eval tests for logic, Rescale, Atan2, Alias, and discrete Delay
1 parent 250e699 commit f02e3cb

2 files changed

Lines changed: 542 additions & 0 deletions

File tree

tests/evals/test_logic_system.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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

Comments
 (0)