diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..acec218 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-24 - CompuCell3D SWIG Exception Overhead in Spatial Loops +**Learning:** Out-of-bounds SWIG object lookups (e.g., `self.cell_field[x, y, 0]`) throw exceptions that incur significant performance overhead inside tightly nested spatial loops. Using `try...except` to catch these exceptions is slow. +**Action:** Use explicit, dynamic boundary checks (e.g., `0 <= x < self.dim.x` and `0 <= y < self.dim.y`) before accessing arrays to prevent SWIG exceptions, rather than relying on `try...except`. diff --git a/Simulation/CancerInvasionSteppables.py b/Simulation/CancerInvasionSteppables.py index 3782ab5..f85b73f 100644 --- a/Simulation/CancerInvasionSteppables.py +++ b/Simulation/CancerInvasionSteppables.py @@ -89,13 +89,11 @@ def initialize_paper_ecm(self): pixels_assigned = 0 for x, y in fiber_pixels: - try: + if 0 <= x < self.dim.x and 0 <= y < self.dim.y: if self.cell_field[x, y, 0] is None: self.cell_field[x, y, 0] = fiber_cell self.fiber_locations.add((x, y)) pixels_assigned += 1 - except: - continue if pixels_assigned >= 10: fibers_created += 1 @@ -175,13 +173,10 @@ def create_paper_cell(self, center_x, center_y, radius): for dy in range(-radius, radius + 1): if dx*dx + dy*dy <= radius*radius: px, py = center_x + dx, center_y + dy - if 0 <= px < 500 and 0 <= py < 500: - try: - if self.cell_field[px, py, 0] is None: - self.cell_field[px, py, 0] = cell - pixels_added += 1 - except: - continue + if 0 <= px < self.dim.x and 0 <= py < self.dim.y: + if self.cell_field[px, py, 0] is None: + self.cell_field[px, py, 0] = cell + pixels_added += 1 if pixels_added >= 20: return True @@ -210,12 +205,9 @@ def safe_cell_removal(self, cell): min(self.dim.x, int(cell.xCOM) + search_radius)): for y in range(max(0, int(cell.yCOM) - search_radius), min(self.dim.y, int(cell.yCOM) + search_radius)): - try: - if self.cell_field[x, y, 0] == cell: - self.cell_field[x, y, 0] = None - pixels_cleared += 1 - except: - continue + if self.cell_field[x, y, 0] == cell: + self.cell_field[x, y, 0] = None + pixels_cleared += 1 return pixels_cleared > 0 except Exception as e: return False @@ -285,16 +277,13 @@ def paper_mmp_system(self): fibers_to_remove = [] for cell in self.cell_list: if cell.type == self.ECMFIBER: - try: - cx, cy = int(cell.xCOM), int(cell.yCOM) - if 0 <= cx < 500 and 0 <= cy < 500: - mmp_conc = mmp_field[cx, cy, 0] - if mmp_conc >= self.degradation_threshold: - fibers_to_remove.append(cell) - # Paper: reduce MMP count by 1 after degradation - mmp_field[cx, cy, 0] = max(0, mmp_conc - 1) - except: - continue + cx, cy = int(cell.xCOM), int(cell.yCOM) + if 0 <= cx < self.dim.x and 0 <= cy < self.dim.y: + mmp_conc = mmp_field[cx, cy, 0] + if mmp_conc >= self.degradation_threshold: + fibers_to_remove.append(cell) + # Paper: reduce MMP count by 1 after degradation + mmp_field[cx, cy, 0] = max(0, mmp_conc - 1) # Remove degraded fibers for fiber in fibers_to_remove: @@ -312,13 +301,10 @@ def check_ecm_contact(self, cell): for dx in range(-3, 4): for dy in range(-3, 4): nx, ny = cx + dx, cy + dy - if 0 <= nx < 500 and 0 <= ny < 500: - try: - neighbor = self.cell_field[nx, ny, 0] - if neighbor and neighbor.type == self.ECMFIBER: - return True - except: - continue + if 0 <= nx < self.dim.x and 0 <= ny < self.dim.y: + neighbor = self.cell_field[nx, ny, 0] + if neighbor and neighbor.type == self.ECMFIBER: + return True return False except: return False diff --git a/Simulation/__pycache__/CancerInvasionSteppables.cpython-312.pyc b/Simulation/__pycache__/CancerInvasionSteppables.cpython-312.pyc deleted file mode 100644 index 611fe30..0000000 Binary files a/Simulation/__pycache__/CancerInvasionSteppables.cpython-312.pyc and /dev/null differ diff --git a/test_optimization.py b/test_optimization.py new file mode 100644 index 0000000..52675d1 --- /dev/null +++ b/test_optimization.py @@ -0,0 +1,87 @@ +import pytest +import sys +from unittest.mock import MagicMock + +# Mock cc3d modules +cc3d_mock = MagicMock() +core_mock = MagicMock() +pysteppables_mock = MagicMock() +class SteppableBasePyMock: + def __init__(self, frequency=1): + self.frequency = frequency +class MitosisSteppableBaseMock: + def __init__(self, frequency=1): + self.frequency = frequency + +pysteppables_mock.SteppableBasePy = SteppableBasePyMock +pysteppables_mock.MitosisSteppableBase = MitosisSteppableBaseMock + +core_mock.PySteppables = pysteppables_mock +cc3d_mock.core = core_mock +sys.modules['cc3d'] = cc3d_mock +sys.modules['cc3d.core'] = core_mock +sys.modules['cc3d.core.PySteppables'] = pysteppables_mock + +# Now we can safely import our module +from CancerInvasionSteppables import CancerInvasionSteppable + +class MockDim: + def __init__(self, x, y): + self.x = x + self.y = y + +class MockCell: + def __init__(self, cell_type, x, y): + self.type = cell_type + self.xCOM = x + self.yCOM = y + +class SafeDict(dict): + def __getitem__(self, key): + return self.get(key, None) + +def test_no_swig_exceptions(): + # Instantiate the steppable + steppable = CancerInvasionSteppable(frequency=1) + + # Mock necessary properties that CC3D injects + steppable.dim = MockDim(500, 500) + steppable.CELL = 1 + steppable.ECMFIBER = 2 + + # Use SafeDict to simulate CC3D's SWIG cell_field + # If the code uses try..except to catch KeyErrors on missing items, this will fail if we don't return None safely for out-of-bounds, + # OR we just populate all possible bounds. + # Actually, the SafeDict mirrors normal `get` behavior which CC3D's SWIG wrapper does when in-bounds, + # but SWIG throws when out of bounds. We want to ensure we don't query out of bounds! + + class StrictBoundDict(dict): + def __init__(self, x_dim, y_dim): + self.x_dim = x_dim + self.y_dim = y_dim + super().__init__() + + def __getitem__(self, key): + x, y, z = key + if x < 0 or x >= self.x_dim or y < 0 or y >= self.y_dim: + raise Exception(f"SWIG IndexError: out of bounds {x}, {y}") + return super().get(key, None) + + steppable.cell_field = StrictBoundDict(steppable.dim.x, steppable.dim.y) + steppable.new_cell = lambda t: MockCell(t, 0, 0) + steppable.cell_list = [] + + # Test create_paper_cell near the boundary to ensure it doesn't throw SWIG error + # With center (495, 495) and radius 6, it will check up to 501, which should be caught by our dynamic boundary check. + result = steppable.create_paper_cell(495, 495, 6) + + # Test check_ecm_contact near boundary + boundary_cell = MockCell(steppable.CELL, 498, 498) + # Should not throw exception + steppable.check_ecm_contact(boundary_cell) + + # Test safe_cell_removal + steppable.safe_cell_removal(boundary_cell) + +if __name__ == "__main__": + pytest.main(["-v", "test_optimization.py"])