Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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`.
52 changes: 19 additions & 33 deletions Simulation/CancerInvasionSteppables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
Binary file not shown.
87 changes: 87 additions & 0 deletions test_optimization.py
Original file line number Diff line number Diff line change
@@ -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"])