From e0bf0b8d797b41ac17a688b8e3da693e4bec6985 Mon Sep 17 00:00:00 2001 From: Alex Greason Date: Wed, 11 Jun 2025 20:47:26 -0500 Subject: [PATCH 1/2] fix simulation stop condition --- nozzlesim/mesh.py | 1 + tests/test_simulate_stop.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/test_simulate_stop.py diff --git a/nozzlesim/mesh.py b/nozzlesim/mesh.py index 77869ff..27d1ed2 100644 --- a/nozzlesim/mesh.py +++ b/nozzlesim/mesh.py @@ -85,6 +85,7 @@ def simulate(self, stop=float("inf")): while event is not None and self.x < stop: self.handleevent(event) event = self.firstevent(self.activeshocks, self.x) + lastcheck = self.remainingangle <= 0 if lastcheck: return diff --git a/tests/test_simulate_stop.py b/tests/test_simulate_stop.py new file mode 100644 index 0000000..9b6301e --- /dev/null +++ b/tests/test_simulate_stop.py @@ -0,0 +1,23 @@ +import nozzlesim.mesh as mesh_module + + +class DummyMesh(mesh_module.Mesh): + def __init__(self): + # initialize with dummy values for parent + super().__init__(1.4, 1.0, [], [], 0, 1) + self.events = ["e1", "e2", "e3"] + self.counter = 0 + + def firstevent(self, shocks, startx): + return self.events.pop(0) if self.events else None + + def handleevent(self, event): + self.counter += 1 + if self.counter == 2: + self.remainingangle = 0 + + +def test_simulate_stops_when_remainingangle_zero(): + m = DummyMesh() + m.simulate() + assert m.counter == 2 From ca50895ba0a40cf5d6553c8c89fe12a22cd18b70 Mon Sep 17 00:00:00 2001 From: Alex Greason Date: Wed, 11 Jun 2025 20:57:17 -0500 Subject: [PATCH 2/2] Implement symmetric arc helper and process simultaneous events --- nozzlesim/mesh.py | 11 +++++++---- nozzlesim/wall.py | 26 ++++++++++++++++++++++++++ tests/test_simulate_stop.py | 5 ++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/nozzlesim/mesh.py b/nozzlesim/mesh.py index 27d1ed2..ea6ac76 100644 --- a/nozzlesim/mesh.py +++ b/nozzlesim/mesh.py @@ -80,14 +80,17 @@ def simulate(self, stop=float("inf")): """Propagate the mesh until no more events occur or ``stop`` is reached.""" event = self.firstevent(self.activeshocks, self.x) - lastcheck = self.remainingangle <= 0 while event is not None and self.x < stop: + currentx = event[1][2].x self.handleevent(event) - event = self.firstevent(self.activeshocks, self.x) - lastcheck = self.remainingangle <= 0 - if lastcheck: + nextevent = self.firstevent(self.activeshocks, self.x) + while nextevent is not None and abs(nextevent[1][2].x - currentx) < epsilon: + self.handleevent(nextevent) + nextevent = self.firstevent(self.activeshocks, self.x) + if self.remainingangle <= 0: return + event = nextevent @staticmethod def sortshocks(shocks, startx): diff --git a/nozzlesim/wall.py b/nozzlesim/wall.py index 2bbad6d..f4d2dae 100644 --- a/nozzlesim/wall.py +++ b/nozzlesim/wall.py @@ -49,6 +49,32 @@ def createarc( segments.append(nextsegment) return segments, start.x + deltax * (numsegments + 1) + @classmethod + def create_symmetric_pair( + cls, + top_start: Point, + bottom_start: Point, + deltax: float, + totalangle: float, + numsegments: int, + ) -> tuple[list["Wall"], list["Wall"], float]: + """Return symmetric wall arcs for the top and bottom nozzle walls. + + ``totalangle`` defines the turn of the top wall. The bottom wall will + mirror the top wall about the horizontal axis so the geometry is exactly + symmetric. + """ + + top_segments, endx = cls.createarc(top_start, deltax, totalangle, numsegments) + bottom_segments: list[Wall] = [] + y_offset = top_start.y + bottom_start.y + for seg in top_segments: + start = Point(seg.start.x, -seg.start.y + y_offset) + end = None if seg.end is None else Point(seg.end.x, -seg.end.y + y_offset) + mirrored = cls(start, -seg.angle, end) + bottom_segments.append(mirrored) + return top_segments, bottom_segments, endx + def exists(self, x: float) -> bool: """Return ``True`` if ``x`` lies between ``start`` and ``end`` (if defined).""" diff --git a/tests/test_simulate_stop.py b/tests/test_simulate_stop.py index 9b6301e..d98c68b 100644 --- a/tests/test_simulate_stop.py +++ b/tests/test_simulate_stop.py @@ -5,7 +5,10 @@ class DummyMesh(mesh_module.Mesh): def __init__(self): # initialize with dummy values for parent super().__init__(1.4, 1.0, [], [], 0, 1) - self.events = ["e1", "e2", "e3"] + # minimal event objects with an ``x`` attribute in the expected place + from nozzlesim.point import Point + + self.events = [["intersection", [None, None, Point(i, 0)]] for i in range(3)] self.counter = 0 def firstevent(self, shocks, startx):