From 70c75d1e6ae412f6e543123c3e51949dca65f4ce Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:32:41 +1300 Subject: [PATCH 01/38] hexagon grid reverted for pytest --- src/tyssue/generation/hexagonal_grids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tyssue/generation/hexagonal_grids.py b/src/tyssue/generation/hexagonal_grids.py index 05e82ea7..d226a3a1 100644 --- a/src/tyssue/generation/hexagonal_grids.py +++ b/src/tyssue/generation/hexagonal_grids.py @@ -13,7 +13,7 @@ def hexa_grid2d(nx, ny, distx, disty, noise=None): """Creates an hexagonal grid of points""" - cy, cx = np.mgrid[0:ny+2, 0:nx+2] + cy, cx = np.mgrid[0:ny, 0:nx] cx = cx.astype(float) cy = cy.astype(float) cx[::2, :] += 0.5 From 9c0fa2203eb4cc752c5a5b83860feefef58e5b6f Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:28:19 +1300 Subject: [PATCH 02/38] update np.alltrue to np.all for NumPy 2.0 compatibility --- tests/topology/test_bulk_topology.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/topology/test_bulk_topology.py b/tests/topology/test_bulk_topology.py index a598cfa9..f6b4c06d 100644 --- a/tests/topology/test_bulk_topology.py +++ b/tests/topology/test_bulk_topology.py @@ -136,8 +136,8 @@ def test_IH_transition(): assert eptm.Nv == Nv + 1 invalid = eptm.get_invalid() - assert np.alltrue(1 - invalid) - assert np.alltrue(eptm.edge_df["sub_vol"] > 0) + assert np.all(1 - invalid) + assert np.all(eptm.edge_df["sub_vol"] > 0) assert ( eptm.face_df[eptm.face_df.segment == "apical"].shape[0] == eptm.cell_df.shape[0] ) @@ -156,7 +156,7 @@ def test_split_vert(): BulkGeometry.update_all(eptm) invalid = eptm.get_invalid() - assert np.alltrue(1 - invalid) + assert np.all(1 - invalid) def test_HI_transition(): @@ -179,8 +179,8 @@ def test_HI_transition(): assert eptm.Nv == Nv invalid = eptm.get_invalid() - assert np.alltrue(1 - invalid) - assert np.alltrue(eptm.edge_df["sub_vol"] > 0) + assert np.all(1 - invalid) + assert np.all(eptm.edge_df["sub_vol"] > 0) def test_find_transitions(): From 842a62ca962d97133b0e3a1e3e492927bcda4552 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:32:16 +1200 Subject: [PATCH 03/38] continue with with renamed local folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4f946e3a..9f39ec24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.py[cod] -# C extensions +# C extensions *.so *.lo *.la From 83b3ed11808d7b510b6303b74ad1be3acad7037f Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:18:23 +1200 Subject: [PATCH 04/38] added solver.single_step_movement instance --- src/tyssue/solvers/viscous.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/tyssue/solvers/viscous.py b/src/tyssue/solvers/viscous.py index fcb4d351..347c0218 100644 --- a/src/tyssue/solvers/viscous.py +++ b/src/tyssue/solvers/viscous.py @@ -152,6 +152,27 @@ def ode_func(self, t, pos): / self.eptm.vert_df.loc[self.eptm.active_verts, "viscosity"].values[:, None] ).ravel() + def single_step_movement(self, tf, dt): + """Solves and returns the final movement vector for a single step of the Euler solver defined above. + + Parameters + ---------- + tf : float, final time when we stop solving + dt : float, time step + on_topo_change : function, optional, default None + function of `self.eptm` + topo_change_args : tuple, arguments passed to `on_topo_change` + + """ + self.eptm.settings["dt"] = dt + for t in np.arange(self.prev_t, tf + dt, dt): + pos = self.current_pos + dot_r = self.ode_func(t, pos) + if self.bounds is not None: + dot_r = np.clip(dot_r, *self.bounds) + movement = dot_r * dt + return movement, dot_r + class IVPSolver: def __init__(self, *args, **kwargs): From 5a4bc3359a11ae16b53244f5584f4dc26f736430 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:46:14 +1200 Subject: [PATCH 05/38] improved single_step_movement to be only need dt --- src/tyssue/solvers/viscous.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tyssue/solvers/viscous.py b/src/tyssue/solvers/viscous.py index 347c0218..36efde0d 100644 --- a/src/tyssue/solvers/viscous.py +++ b/src/tyssue/solvers/viscous.py @@ -152,12 +152,11 @@ def ode_func(self, t, pos): / self.eptm.vert_df.loc[self.eptm.active_verts, "viscosity"].values[:, None] ).ravel() - def single_step_movement(self, tf, dt): + def single_step_movement(self, dt): """Solves and returns the final movement vector for a single step of the Euler solver defined above. Parameters ---------- - tf : float, final time when we stop solving dt : float, time step on_topo_change : function, optional, default None function of `self.eptm` @@ -165,7 +164,7 @@ def single_step_movement(self, tf, dt): """ self.eptm.settings["dt"] = dt - for t in np.arange(self.prev_t, tf + dt, dt): + for t in np.arange(self.prev_t, dt, dt): pos = self.current_pos dot_r = self.ode_func(t, pos) if self.bounds is not None: From 6d3dbb49c23cab9cc7d42c66ad03eb12b779e53c Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:15:41 +1200 Subject: [PATCH 06/38] Added a new parameter to control if reindex happens or not --- src/tyssue/topology/sheet_topology.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tyssue/topology/sheet_topology.py b/src/tyssue/topology/sheet_topology.py index 372533bc..6fc6cf53 100644 --- a/src/tyssue/topology/sheet_topology.py +++ b/src/tyssue/topology/sheet_topology.py @@ -57,7 +57,7 @@ def split_vert( return new_edges -def type1_transition(sheet, edge01, *, remove_tri_faces=True, multiplier=1.5): +def type1_transition(sheet, edge01, *, do_reindex =True, remove_tri_faces=True, multiplier=1.5): """Performs a type 1 transition around the edge edge01 See ../../doc/illus/t1_transition.png for a sketch of the definition @@ -86,7 +86,7 @@ def type1_transition(sheet, edge01, *, remove_tri_faces=True, multiplier=1.5): srce, trgt, face = sheet.edge_df.loc[edge01, ["srce", "trgt", "face"]].astype(int) vert = min(srce, trgt) # find the vertex that won't be reindexed - ret_code = collapse_edge(sheet, edge01, reindex=True, allow_two_sided=True) + ret_code = collapse_edge(sheet, edge01, reindex=do_reindex, allow_two_sided=True) if ret_code < 0: warnings.warn(f"Collapse of edge {edge01} failed") return ret_code @@ -96,7 +96,7 @@ def type1_transition(sheet, edge01, *, remove_tri_faces=True, multiplier=1.5): vert, face, multiplier=multiplier, - reindex=True, + reindex=do_reindex, recenter=True, ) From efd9d961418f90467b7e62405b07227510b170b2 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:51:08 +1200 Subject: [PATCH 07/38] added drop_face --- src/tyssue/topology/base_topology.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tyssue/topology/base_topology.py b/src/tyssue/topology/base_topology.py index 920f527d..33312d49 100644 --- a/src/tyssue/topology/base_topology.py +++ b/src/tyssue/topology/base_topology.py @@ -183,6 +183,14 @@ def drop_two_sided_faces(eptm): eptm.face_df.drop(two_sided, axis=0, inplace=True) +def drop_face(sheet, face, **kwargs): + """ + Removes the face indexed by "face" and all associated edges + """ + edge = sheet.edge_df.loc[(sheet.edge_df['face'] == face)].index + print(f"Dropping face '{face}'") + sheet.remove(edge, **kwargs) + def remove_face(sheet, face): """Removes a face from the mesh. From 88a362b63df2f64384cdf2ba88517b4ca4dae43d Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:52:13 +1200 Subject: [PATCH 08/38] added T2Swap --- src/tyssue/behaviors/sheet/basic_events.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index b47ee9a7..d0a70e2a 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -8,6 +8,7 @@ from ...geometry.sheet_geometry import SheetGeometry from ...topology.sheet_topology import cell_division +from ...topology.base_topology import drop_face from ...utils.decorators import face_lookup from .actions import ( decrease, @@ -248,3 +249,19 @@ def contraction_line_tension(sheet, manager, **kwargs): isotropic=True, limit=100, ) + + +def T2Swap(sheet, manager, face_id, crit_area): + """ + A behaviour function of the T2 transition that should be added to the manager during simulation. + It removes the face with cell_id is triangular and its area is smaller than crit_area. + """ + if (sheet.face_df.loc[face_id,'num_sides']) < 4 and sheet.face_df.loc[face_id, 'area'] < crit_area: + drop_face(sheet, face_id) + print(f'Removed face {face_id}') + else: + # Use the stable `id` column instead of relying on positional index + stable_id = sheet.face_df.loc[face_id, 'id'] + manager.append(T2Swap, cell_id=stable_id, crit_area=crit_area) + + From 11820a2e66ab1d67840cb24f5cdf9fa17fc617d0 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:24:21 +1200 Subject: [PATCH 09/38] bug fix: using face_id consistently for 2D sheet now --- src/tyssue/behaviors/sheet/basic_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index d0a70e2a..405b9262 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -254,7 +254,7 @@ def contraction_line_tension(sheet, manager, **kwargs): def T2Swap(sheet, manager, face_id, crit_area): """ A behaviour function of the T2 transition that should be added to the manager during simulation. - It removes the face with cell_id is triangular and its area is smaller than crit_area. + It removes the face with face_id is triangular and its area is smaller than crit_area. """ if (sheet.face_df.loc[face_id,'num_sides']) < 4 and sheet.face_df.loc[face_id, 'area'] < crit_area: drop_face(sheet, face_id) @@ -262,6 +262,6 @@ def T2Swap(sheet, manager, face_id, crit_area): else: # Use the stable `id` column instead of relying on positional index stable_id = sheet.face_df.loc[face_id, 'id'] - manager.append(T2Swap, cell_id=stable_id, crit_area=crit_area) + manager.append(T2Swap, face_id=stable_id, crit_area=crit_area) From d9dd0b38916918eec2fe8d8f525b04842e3f6f56 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 23 Sep 2025 21:43:17 +1200 Subject: [PATCH 10/38] created cell_class_events.py --- .../behaviors/sheet/cell_class_events.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/tyssue/behaviors/sheet/cell_class_events.py diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py new file mode 100644 index 00000000..55c5f98b --- /dev/null +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -0,0 +1,74 @@ +""" +Event module for cell class transition rules, modify the details accordingly to your model. +======================= + +""" + +from ...geometry.sheet_geometry import SheetGeometry +from ...topology.sheet_topology import cell_division + +def cell_cycle_transition(sheet, manager, dt, cell_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): + """ + Controls cell class state transitions for cell cycle based on timers and probabilities. + + Parameters + ---------- + sheet: tyssue.Sheet + The tissue sheet. + manager: EventManager + The event manager scheduling the behaviour. + cell_id: Integer + ID of the cell being controlled. + p_recruit: float + Probability for an 'S' cell to be recruited to 'G2'. + dt: float + Time step increment. + G2_duration: float + Fixed duration cells stay in G2 phase. + G1_duration: float + Fixed duration cells stay in G1 phase. + """ + + # Record the current cell class + current_class = sheet.face_df.loc[cell_id, 'cell_class'] + # (1) Recruit mature 'S' cells into G2 with probability p_recruit + if current_class == 'S': + if np.random.rand() < p_recruit: + sheet.face_df.loc[cell_id, 'cell_class'] = 'G2' + sheet.face_df.loc[cell_id, 'timer'] = G2_duration + # append to next deque + manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + + # (2) Decrement timers for cells in G2; when timer ends, move to M + elif current_class == 'G2': + sheet.face_df.loc[cell_id, 'timer'] -= dt + if sheet.face_df.loc[cell_id, 'timer'] <= 0: + sheet.face_df.loc[cell_id, 'cell_class'] = 'M' + # append to next deque + manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + + # (3) For cells in M, perform division and set daughters to G1 with timer + elif current_class == 'M': + daughter = cell_division(sheet, mother=cell_id, geom = SheetGeometry ) + # Set parent and daughter to G1 with G1 timer + sheet.face_df.loc[cell_id, 'cell_class'] = 'G1' + sheet.face_df.loc[daughter, 'cell_class'] = 'G1' + sheet.face_df.loc[cell_id, 'timer'] = G1_duration + sheet.face_df.loc[daughter, 'timer'] = G1_duration + # append to next deque + manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + manager.append(cell_cycle_transition, dt=dt, cell_id=daughter) + + # (4) Decrement timers for G1 cells; when timer ends, move to S + elif current_class == 'G1': + sheet.face_df.loc[cell_id, 'timer'] -= dt + if sheet.face_df.loc[cell_id, 'timer'] <= 0: + sheet.face_df.loc[cell_id, 'cell_class'] = 'S' + # append to next deque + manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + + + + + + From d9ce6f6c48383200d343cd14426d3b95d34cb172 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:11:23 +1200 Subject: [PATCH 11/38] naive version of a multi-class cell system --- .../behaviors/sheet/cell_class_events.py | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index 55c5f98b..d670f4ac 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -4,10 +4,11 @@ """ -from ...geometry.sheet_geometry import SheetGeometry +import numpy as np +from ...geometry.planar_geometry import PlanarGeometry from ...topology.sheet_topology import cell_division -def cell_cycle_transition(sheet, manager, dt, cell_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): +def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): """ Controls cell class state transitions for cell cycle based on timers and probabilities. @@ -17,7 +18,7 @@ def cell_cycle_transition(sheet, manager, dt, cell_id, p_recruit=0.1, G2_duratio The tissue sheet. manager: EventManager The event manager scheduling the behaviour. - cell_id: Integer + face_id: Integer ID of the cell being controlled. p_recruit: float Probability for an 'S' cell to be recruited to 'G2'. @@ -30,42 +31,42 @@ def cell_cycle_transition(sheet, manager, dt, cell_id, p_recruit=0.1, G2_duratio """ # Record the current cell class - current_class = sheet.face_df.loc[cell_id, 'cell_class'] + current_class = sheet.face_df.loc[face_id, 'cell_class'] # (1) Recruit mature 'S' cells into G2 with probability p_recruit if current_class == 'S': if np.random.rand() < p_recruit: - sheet.face_df.loc[cell_id, 'cell_class'] = 'G2' - sheet.face_df.loc[cell_id, 'timer'] = G2_duration + sheet.face_df.loc[face_id, 'cell_class'] = 'G2' + sheet.face_df.loc[face_id, 'timer'] = G2_duration # append to next deque - manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + manager.append(cell_cycle_transition, dt=dt, face_id=face_id) # (2) Decrement timers for cells in G2; when timer ends, move to M elif current_class == 'G2': - sheet.face_df.loc[cell_id, 'timer'] -= dt - if sheet.face_df.loc[cell_id, 'timer'] <= 0: - sheet.face_df.loc[cell_id, 'cell_class'] = 'M' + sheet.face_df.loc[face_id, 'timer'] -= dt + if sheet.face_df.loc[face_id, 'timer'] <= 0: + sheet.face_df.loc[face_id, 'cell_class'] = 'M' # append to next deque - manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + manager.append(cell_cycle_transition, dt=dt, face_id=face_id) # (3) For cells in M, perform division and set daughters to G1 with timer elif current_class == 'M': - daughter = cell_division(sheet, mother=cell_id, geom = SheetGeometry ) + daughter = cell_division(sheet, mother=face_id, geom = PlanarGeometry ) # Set parent and daughter to G1 with G1 timer - sheet.face_df.loc[cell_id, 'cell_class'] = 'G1' + sheet.face_df.loc[face_id, 'cell_class'] = 'G1' sheet.face_df.loc[daughter, 'cell_class'] = 'G1' - sheet.face_df.loc[cell_id, 'timer'] = G1_duration + sheet.face_df.loc[face_id, 'timer'] = G1_duration sheet.face_df.loc[daughter, 'timer'] = G1_duration # append to next deque - manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) - manager.append(cell_cycle_transition, dt=dt, cell_id=daughter) + manager.append(cell_cycle_transition, dt=dt, face_id=face_id) + manager.append(cell_cycle_transition, dt=dt, face_id=daughter) # (4) Decrement timers for G1 cells; when timer ends, move to S elif current_class == 'G1': - sheet.face_df.loc[cell_id, 'timer'] -= dt - if sheet.face_df.loc[cell_id, 'timer'] <= 0: - sheet.face_df.loc[cell_id, 'cell_class'] = 'S' + sheet.face_df.loc[face_id, 'timer'] -= dt + if sheet.face_df.loc[face_id, 'timer'] <= 0: + sheet.face_df.loc[face_id, 'cell_class'] = 'S' # append to next deque - manager.append(cell_cycle_transition, dt=dt, cell_id=cell_id) + manager.append(cell_cycle_transition, dt=dt, face_id=face_id) From 96b0e1a381d38532fc54580d918376848a2da23f Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:23:21 +1300 Subject: [PATCH 12/38] Bug fix: index look up can now work with edge and vertex df not so sure about cell data frame, leave it in the "else" case, which is logically the same as unchanged. --- src/tyssue/core/objects.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tyssue/core/objects.py b/src/tyssue/core/objects.py index ac07db0a..1e47fd24 100644 --- a/src/tyssue/core/objects.py +++ b/src/tyssue/core/objects.py @@ -570,7 +570,8 @@ def get_orbits(self, center, periph): return orbits def idx_lookup(self, elem_id, element): - """returns the current index of the element with the `"id"` column equal to `elem_id` + """returns the current index of the element with the `"id"` column equal to `elem_id`; + thse stable IDs are called "ID" in face data frame, but called "unique_id" in vertex and edge data frames Parameters ---------- @@ -579,7 +580,10 @@ def idx_lookup(self, elem_id, element): element : {"vert"|"edge"|"face"|"cell"} the corresponding dataset. """ - df = self.datasets[element]["id"] + if element == "vert" or element == "edge": + df = self.datasets[element]['unique_id'] + else: + df = self.datasets[element]["id"] idx = df[df == elem_id].index if len(idx): return idx[0] From afa5ee1ad4b4e6381cb545b9b827940fa91ca89b Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:47:19 +1300 Subject: [PATCH 13/38] docstring update --- src/tyssue/topology/sheet_topology.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tyssue/topology/sheet_topology.py b/src/tyssue/topology/sheet_topology.py index 6fc6cf53..45159872 100644 --- a/src/tyssue/topology/sheet_topology.py +++ b/src/tyssue/topology/sheet_topology.py @@ -70,9 +70,8 @@ def type1_transition(sheet, edge01, *, do_reindex =True, remove_tri_faces=True, sheet : a `Sheet` instance edge_01 : int index of the edge around which the transition takes place - epsilon : float, optional, deprecated - default 0.1, the initial length of the new edge, in case "threshold_length" - is not in the sheet.settings + do_reindex : bool, optional + whether or not to reindex the sheet. remove_tri_faces : bool, optional if True (the default), will remove triangular cells after the T1 transition is performed @@ -80,7 +79,6 @@ def type1_transition(sheet, edge01, *, do_reindex =True, remove_tri_faces=True, default 1.5, the multiplier to the threshold length, so that the length of the new edge is set to multiplier * threshold_length - """ srce, trgt, face = sheet.edge_df.loc[edge01, ["srce", "trgt", "face"]].astype(int) From 7a5665db19888a60e61fd95aa928884ee26e41c5 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:50:30 +1300 Subject: [PATCH 14/38] primary version of T1 event created need to double check the logic with the use of stable_id --- src/tyssue/behaviors/sheet/basic_events.py | 40 ++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 405b9262..94335719 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -7,7 +7,7 @@ import logging from ...geometry.sheet_geometry import SheetGeometry -from ...topology.sheet_topology import cell_division +from ...topology.sheet_topology import cell_division, type1_transition as T1_transition from ...topology.base_topology import drop_face from ...utils.decorators import face_lookup from .actions import ( @@ -251,17 +251,45 @@ def contraction_line_tension(sheet, manager, **kwargs): ) -def T2Swap(sheet, manager, face_id, crit_area): +def T1Swap(sheet,manager, geom, stable_face, T1_threshold, multiplier, crit_area): + """ + A behaviour function of the T1 transition that performs a T1 swap on an edge that is shorter than T1_threshold. + """ + # First, we need to look up the current index of stable_face in face_df. + face_id = sheet.idx_lookup(stable_face, "face") + # If the polygon has less than 4 sides, we append it to T2 swap. + if sheet.face_df.loc[face_id,'num_sides'] < 4: + manager.append(T2Swap, face_id=face_id, crit_area=crit_area) + else: + # If the polygon has more than 3 sides, then we perform t1 transition of its edges according to length. + edges_df = sheet.edge_df[sheet.edge_df["face"] == face_id] + for idx in edges_df.index: + if edges_df.loc[i,'length'] < T1_threshold: + T1_transition(sheet, idx, do_reindex=False, remove_tri_faces=False, multiplier=multiplier) + geom.update_all(sheet) + else: + continue + # After the edge length loop, append the polygon to the manager for next time step. + manager.append(T1Swap, face_id=face_id, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) + + + +def T2Swap(sheet, manager, stable_face, crit_area): """ A behaviour function of the T2 transition that should be added to the manager during simulation. - It removes the face with face_id is triangular and its area is smaller than crit_area. + It removes the face with stable_face (unique ID) is triangular and its area is smaller than crit_area. """ - if (sheet.face_df.loc[face_id,'num_sides']) < 4 and sheet.face_df.loc[face_id, 'area'] < crit_area: + # First, we need to look up the current index of stable_face in face_df. + face_id = sheet.idx_lookup(stable_face, "face") + if (sheet.face_df.loc[face_id,'num_sides']) == 3 and sheet.face_df.loc[face_id, 'area'] < crit_area: + drop_face(sheet, face_id) + print(f'Removed triangular face {face_id}') + elif sheet.face_df.loc[face_id,'num_sides'] < 3: drop_face(sheet, face_id) - print(f'Removed face {face_id}') + print(f'Removed invalid face {face_id}') else: # Use the stable `id` column instead of relying on positional index stable_id = sheet.face_df.loc[face_id, 'id'] - manager.append(T2Swap, face_id=stable_id, crit_area=crit_area) + manager.append(T2Swap, face_id=face_id, crit_area=crit_area) From f2d9ed1915859c5b550cd22ffac1db6a5c53476e Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:46:06 +1300 Subject: [PATCH 15/38] stable ID is used as parameter --- src/tyssue/behaviors/sheet/basic_events.py | 6 +++--- src/tyssue/behaviors/sheet/cell_class_events.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 94335719..c9d12222 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -259,7 +259,7 @@ def T1Swap(sheet,manager, geom, stable_face, T1_threshold, multiplier, crit_area face_id = sheet.idx_lookup(stable_face, "face") # If the polygon has less than 4 sides, we append it to T2 swap. if sheet.face_df.loc[face_id,'num_sides'] < 4: - manager.append(T2Swap, face_id=face_id, crit_area=crit_area) + manager.append(T2Swap, face_id=stable_face, crit_area=crit_area) else: # If the polygon has more than 3 sides, then we perform t1 transition of its edges according to length. edges_df = sheet.edge_df[sheet.edge_df["face"] == face_id] @@ -270,7 +270,7 @@ def T1Swap(sheet,manager, geom, stable_face, T1_threshold, multiplier, crit_area else: continue # After the edge length loop, append the polygon to the manager for next time step. - manager.append(T1Swap, face_id=face_id, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) + manager.append(T1Swap, face_id=stable_face, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) @@ -290,6 +290,6 @@ def T2Swap(sheet, manager, stable_face, crit_area): else: # Use the stable `id` column instead of relying on positional index stable_id = sheet.face_df.loc[face_id, 'id'] - manager.append(T2Swap, face_id=face_id, crit_area=crit_area) + manager.append(T2Swap, face_id=stable_face, crit_area=crit_area) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index d670f4ac..fdc33e13 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -8,7 +8,7 @@ from ...geometry.planar_geometry import PlanarGeometry from ...topology.sheet_topology import cell_division -def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): +def cell_cycle_transition(sheet, manager, dt, stable_face, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): """ Controls cell class state transitions for cell cycle based on timers and probabilities. @@ -29,7 +29,8 @@ def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duratio G1_duration: float Fixed duration cells stay in G1 phase. """ - + # First, we need to look up the current index of stable_face in face_df. + face_id = sheet.idx_lookup(stable_face, "face") # Record the current cell class current_class = sheet.face_df.loc[face_id, 'cell_class'] # (1) Recruit mature 'S' cells into G2 with probability p_recruit @@ -66,7 +67,7 @@ def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duratio if sheet.face_df.loc[face_id, 'timer'] <= 0: sheet.face_df.loc[face_id, 'cell_class'] = 'S' # append to next deque - manager.append(cell_cycle_transition, dt=dt, face_id=face_id) + manager.append(cell_cycle_transition, dt=dt, stable_face= stable_face) From c4961e7869f9d7f285727ba8e67d85ddb71bd839 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:01:34 +1300 Subject: [PATCH 16/38] looks up unique_id in data frames --- src/tyssue/core/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tyssue/core/objects.py b/src/tyssue/core/objects.py index 1e47fd24..625dcf2d 100644 --- a/src/tyssue/core/objects.py +++ b/src/tyssue/core/objects.py @@ -580,7 +580,7 @@ def idx_lookup(self, elem_id, element): element : {"vert"|"edge"|"face"|"cell"} the corresponding dataset. """ - if element == "vert" or element == "edge": + if element in ['vert', 'edge','face']: df = self.datasets[element]['unique_id'] else: df = self.datasets[element]["id"] From baf9abddd214a7a1a541183320ffb0f7e5e038c1 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:03:55 +1300 Subject: [PATCH 17/38] unique_id is also updated after face division now --- src/tyssue/topology/sheet_topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tyssue/topology/sheet_topology.py b/src/tyssue/topology/sheet_topology.py index 45159872..7b872ca7 100644 --- a/src/tyssue/topology/sheet_topology.py +++ b/src/tyssue/topology/sheet_topology.py @@ -206,7 +206,7 @@ def face_division(sheet, mother, vert_a, vert_b): sheet.face_df = pd.concat([sheet.face_df, face_cols], ignore_index=True) sheet.face_df.index.name = "face" daughter = int(sheet.face_df.index[-1]) - + sheet.face_df.loc[daughter, 'unique_id'] = sheet.face_df['unique_id'].max() + 1 edge_cols = sheet.edge_df[sheet.edge_df["face"] == mother].iloc[0:1] sheet.edge_df = pd.concat([sheet.edge_df, edge_cols, edge_cols], ignore_index=True) From 6d789ad07d0935e41d08f1e707ce8add8da16023 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:05:03 +1300 Subject: [PATCH 18/38] variable idx is used to distinguish index and unique ID --- .../behaviors/sheet/cell_class_events.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index fdc33e13..98608141 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -8,7 +8,7 @@ from ...geometry.planar_geometry import PlanarGeometry from ...topology.sheet_topology import cell_division -def cell_cycle_transition(sheet, manager, dt, stable_face, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): +def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): """ Controls cell class state transitions for cell cycle based on timers and probabilities. @@ -29,45 +29,48 @@ def cell_cycle_transition(sheet, manager, dt, stable_face, p_recruit=0.1, G2_dur G1_duration: float Fixed duration cells stay in G1 phase. """ - # First, we need to look up the current index of stable_face in face_df. - face_id = sheet.idx_lookup(stable_face, "face") + # First, we need to look up the current index of the face with face ID. + idx = sheet.idx_lookup(face_id, "face") # Record the current cell class - current_class = sheet.face_df.loc[face_id, 'cell_class'] + current_class = sheet.face_df.loc[idx, 'cell_class'] # (1) Recruit mature 'S' cells into G2 with probability p_recruit if current_class == 'S': if np.random.rand() < p_recruit: - sheet.face_df.loc[face_id, 'cell_class'] = 'G2' - sheet.face_df.loc[face_id, 'timer'] = G2_duration + sheet.face_df.loc[idx, 'cell_class'] = 'G2' + sheet.face_df.loc[idx, 'timer'] = G2_duration # append to next deque manager.append(cell_cycle_transition, dt=dt, face_id=face_id) # (2) Decrement timers for cells in G2; when timer ends, move to M elif current_class == 'G2': - sheet.face_df.loc[face_id, 'timer'] -= dt - if sheet.face_df.loc[face_id, 'timer'] <= 0: - sheet.face_df.loc[face_id, 'cell_class'] = 'M' + sheet.face_df.loc[idx, 'timer'] -= dt + if sheet.face_df.loc[idx, 'timer'] <= 0: + sheet.face_df.loc[idx, 'cell_class'] = 'M' # append to next deque manager.append(cell_cycle_transition, dt=dt, face_id=face_id) # (3) For cells in M, perform division and set daughters to G1 with timer elif current_class == 'M': daughter = cell_division(sheet, mother=face_id, geom = PlanarGeometry ) - # Set parent and daughter to G1 with G1 timer - sheet.face_df.loc[face_id, 'cell_class'] = 'G1' + # Set parent and daughter to G1 with G1 timer, note that variable daughter is the index of the new row already. + sheet.face_df.loc[idx, 'cell_class'] = 'G1' sheet.face_df.loc[daughter, 'cell_class'] = 'G1' - sheet.face_df.loc[face_id, 'timer'] = G1_duration + sheet.face_df.loc[idx, 'timer'] = G1_duration sheet.face_df.loc[daughter, 'timer'] = G1_duration # append to next deque manager.append(cell_cycle_transition, dt=dt, face_id=face_id) - manager.append(cell_cycle_transition, dt=dt, face_id=daughter) + # look up the unique id of daughter, then append. + daughter_id = sheet.face_df.loc[daughter, 'unique_id'] + print(f'daughter index {daughter} with unique ID: {daughter_id} is generated....') + manager.append(cell_cycle_transition, dt=dt, face_id=daughter_id) # (4) Decrement timers for G1 cells; when timer ends, move to S elif current_class == 'G1': - sheet.face_df.loc[face_id, 'timer'] -= dt - if sheet.face_df.loc[face_id, 'timer'] <= 0: - sheet.face_df.loc[face_id, 'cell_class'] = 'S' + sheet.face_df.loc[idx, 'timer'] -= dt + if sheet.face_df.loc[idx, 'timer'] <= 0: + sheet.face_df.loc[idx, 'cell_class'] = 'S' # append to next deque - manager.append(cell_cycle_transition, dt=dt, stable_face= stable_face) + manager.append(cell_cycle_transition, dt=dt, face_id = face_id) From f5dedd8f8bc6e06330921f07b38690561bf1b000 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:26:19 +1300 Subject: [PATCH 19/38] bug fix: not mix-using the variable idx and face for cell division function --- src/tyssue/behaviors/sheet/cell_class_events.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index 98608141..fa3bae4b 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -51,7 +51,8 @@ def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duratio # (3) For cells in M, perform division and set daughters to G1 with timer elif current_class == 'M': - daughter = cell_division(sheet, mother=face_id, geom = PlanarGeometry ) + # Make sure we pass the variable idx for cell division. + daughter = cell_division(sheet, mother=idx, geom = PlanarGeometry ) # Set parent and daughter to G1 with G1 timer, note that variable daughter is the index of the new row already. sheet.face_df.loc[idx, 'cell_class'] = 'G1' sheet.face_df.loc[daughter, 'cell_class'] = 'G1' From 261ad1818e2621636b348051fc8fc74132ff7dc6 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:23:47 +1300 Subject: [PATCH 20/38] improved the event function for T1 and T2 --- src/tyssue/behaviors/sheet/basic_events.py | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index c9d12222..02eeac27 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -251,45 +251,45 @@ def contraction_line_tension(sheet, manager, **kwargs): ) -def T1Swap(sheet,manager, geom, stable_face, T1_threshold, multiplier, crit_area): +def T1Swap(sheet,manager, geom, face_id, T1_threshold, multiplier, crit_area): """ A behaviour function of the T1 transition that performs a T1 swap on an edge that is shorter than T1_threshold. """ - # First, we need to look up the current index of stable_face in face_df. - face_id = sheet.idx_lookup(stable_face, "face") + # First, we need to look up the current index of the face with face ID. + idx = sheet.idx_lookup(face_id, "face") # If the polygon has less than 4 sides, we append it to T2 swap. - if sheet.face_df.loc[face_id,'num_sides'] < 4: - manager.append(T2Swap, face_id=stable_face, crit_area=crit_area) + if sheet.face_df.loc[idx,'num_sides'] < 4: + manager.append(T2Swap, face_id=face_id, crit_area=crit_area) else: # If the polygon has more than 3 sides, then we perform t1 transition of its edges according to length. edges_df = sheet.edge_df[sheet.edge_df["face"] == face_id] - for idx in edges_df.index: + for i in edges_df.index: if edges_df.loc[i,'length'] < T1_threshold: - T1_transition(sheet, idx, do_reindex=False, remove_tri_faces=False, multiplier=multiplier) + T1_transition(sheet, i, do_reindex=False, remove_tri_faces=False, multiplier=multiplier) geom.update_all(sheet) else: continue # After the edge length loop, append the polygon to the manager for next time step. - manager.append(T1Swap, face_id=stable_face, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) + manager.append(T1Swap, face_id=face_id, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) -def T2Swap(sheet, manager, stable_face, crit_area): +def T2Swap(sheet, manager, face_id, crit_area): """ A behaviour function of the T2 transition that should be added to the manager during simulation. It removes the face with stable_face (unique ID) is triangular and its area is smaller than crit_area. """ # First, we need to look up the current index of stable_face in face_df. - face_id = sheet.idx_lookup(stable_face, "face") - if (sheet.face_df.loc[face_id,'num_sides']) == 3 and sheet.face_df.loc[face_id, 'area'] < crit_area: - drop_face(sheet, face_id) - print(f'Removed triangular face {face_id}') - elif sheet.face_df.loc[face_id,'num_sides'] < 3: - drop_face(sheet, face_id) - print(f'Removed invalid face {face_id}') + idx = sheet.idx_lookup(face_id, "face") + if (sheet.face_df.loc[idx,'num_sides']) == 3 and sheet.face_df.loc[idx, 'area'] < crit_area: + drop_face(sheet, idx) + print(f'Removed triangular face ID: {face_id}') + elif sheet.face_df.loc[idx,'num_sides'] < 3: + drop_face(sheet, idx) + print(f'Removed invalid face ID: {face_id}') else: # Use the stable `id` column instead of relying on positional index - stable_id = sheet.face_df.loc[face_id, 'id'] - manager.append(T2Swap, face_id=stable_face, crit_area=crit_area) + face_id = sheet.face_df.loc[face_id, 'unique_id'] + manager.append(T2Swap, face_id=face_id, crit_area=crit_area) From 86acfba7801366574582028104193ead511ea246 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:06:56 +1300 Subject: [PATCH 21/38] bug fix on T1swap and T2swap used t1_threshold consistently for T1Swap function; improved the printout message in T2Swap --- src/tyssue/behaviors/sheet/basic_events.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 02eeac27..999fcf59 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -251,7 +251,7 @@ def contraction_line_tension(sheet, manager, **kwargs): ) -def T1Swap(sheet,manager, geom, face_id, T1_threshold, multiplier, crit_area): +def T1Swap(sheet,manager, face_id, geom, t1_threshold, multiplier, crit_area): """ A behaviour function of the T1 transition that performs a T1 swap on an edge that is shorter than T1_threshold. """ @@ -262,15 +262,16 @@ def T1Swap(sheet,manager, geom, face_id, T1_threshold, multiplier, crit_area): manager.append(T2Swap, face_id=face_id, crit_area=crit_area) else: # If the polygon has more than 3 sides, then we perform t1 transition of its edges according to length. - edges_df = sheet.edge_df[sheet.edge_df["face"] == face_id] + edges_df = sheet.edge_df[sheet.edge_df["face"] == idx] for i in edges_df.index: - if edges_df.loc[i,'length'] < T1_threshold: + if edges_df.loc[i,'length'] < t1_threshold: T1_transition(sheet, i, do_reindex=False, remove_tri_faces=False, multiplier=multiplier) geom.update_all(sheet) + print(f'Performed T1 swap on face with unique ID: {face_id}') else: continue # After the edge length loop, append the polygon to the manager for next time step. - manager.append(T1Swap, face_id=face_id, T1_threshold= T1_threshold, multiplier = multiplier, crit_area=crit_area) + manager.append(T1Swap, face_id=face_id, geom = geom, t1_threshold = t1_threshold, multiplier = multiplier, crit_area=crit_area) @@ -283,10 +284,10 @@ def T2Swap(sheet, manager, face_id, crit_area): idx = sheet.idx_lookup(face_id, "face") if (sheet.face_df.loc[idx,'num_sides']) == 3 and sheet.face_df.loc[idx, 'area'] < crit_area: drop_face(sheet, idx) - print(f'Removed triangular face ID: {face_id}') + print(f'Removed triangular face with unique ID: {face_id}') elif sheet.face_df.loc[idx,'num_sides'] < 3: drop_face(sheet, idx) - print(f'Removed invalid face ID: {face_id}') + print(f'Removed invalid face with unique ID: {face_id}') else: # Use the stable `id` column instead of relying on positional index face_id = sheet.face_df.loc[face_id, 'unique_id'] From 61f5be5ccaeeb3ba173f921e195da9a50c0aafd2 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:08:44 +1300 Subject: [PATCH 22/38] changes made: the epithelium now resets index and resets topology relationships after manager executes a deque --- src/tyssue/solvers/viscous.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tyssue/solvers/viscous.py b/src/tyssue/solvers/viscous.py index 36efde0d..86513001 100644 --- a/src/tyssue/solvers/viscous.py +++ b/src/tyssue/solvers/viscous.py @@ -122,6 +122,8 @@ def solve(self, tf, dt, on_topo_change=None, topo_change_args=()): self.prev_t = t if self.manager is not None: self.manager.execute(self.eptm) + self.eptm.reset_index() + self.eptm.reset_topo() self.geom.update_all(self.eptm) self.manager.update() From c01a56585af659195c6a1e48db780d58b3af9681 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:56:00 +1300 Subject: [PATCH 23/38] rewrote T2Swap --- src/tyssue/behaviors/sheet/basic_events.py | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 999fcf59..91be13d6 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -275,22 +275,18 @@ def T1Swap(sheet,manager, face_id, geom, t1_threshold, multiplier, crit_area): -def T2Swap(sheet, manager, face_id, crit_area): +def T2Swap(sheet, manager, crit_area): """ A behaviour function of the T2 transition that should be added to the manager during simulation. - It removes the face with stable_face (unique ID) is triangular and its area is smaller than crit_area. + It removes the face if it is triangular and its area is smaller than crit_area. """ - # First, we need to look up the current index of stable_face in face_df. - idx = sheet.idx_lookup(face_id, "face") - if (sheet.face_df.loc[idx,'num_sides']) == 3 and sheet.face_df.loc[idx, 'area'] < crit_area: - drop_face(sheet, idx) - print(f'Removed triangular face with unique ID: {face_id}') - elif sheet.face_df.loc[idx,'num_sides'] < 3: - drop_face(sheet, idx) - print(f'Removed invalid face with unique ID: {face_id}') - else: - # Use the stable `id` column instead of relying on positional index - face_id = sheet.face_df.loc[face_id, 'unique_id'] - manager.append(T2Swap, face_id=face_id, crit_area=crit_area) + face_dataframe = sheet.face_df + Face_list = face_dataframe.loc[(face_dataframe['num_sides'] < 4) & (face_dataframe['area'] < crit_area)].index.tolist() + print(f'Face list: {Face_list}') + for face in Face_list: + drop_face(sheet, face) + manager.append(T2Swap, crit_area = crit_area) + + From d8d46fa51d07542578752215a91ac3dc68566716 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:35:05 +1300 Subject: [PATCH 24/38] changed the function T1Swap now the function takes advantage of both the unique ID and dataframe index --- src/tyssue/behaviors/sheet/basic_events.py | 34 ++++++++++------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 91be13d6..88b389a3 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -251,28 +251,24 @@ def contraction_line_tension(sheet, manager, **kwargs): ) -def T1Swap(sheet,manager, face_id, geom, t1_threshold, multiplier, crit_area): +def T1Swap(sheet,manager, t1_threshold, multiplier): """ A behaviour function of the T1 transition that performs a T1 swap on an edge that is shorter than T1_threshold. """ - # First, we need to look up the current index of the face with face ID. - idx = sheet.idx_lookup(face_id, "face") - # If the polygon has less than 4 sides, we append it to T2 swap. - if sheet.face_df.loc[idx,'num_sides'] < 4: - manager.append(T2Swap, face_id=face_id, crit_area=crit_area) - else: - # If the polygon has more than 3 sides, then we perform t1 transition of its edges according to length. - edges_df = sheet.edge_df[sheet.edge_df["face"] == idx] - for i in edges_df.index: - if edges_df.loc[i,'length'] < t1_threshold: - T1_transition(sheet, i, do_reindex=False, remove_tri_faces=False, multiplier=multiplier) - geom.update_all(sheet) - print(f'Performed T1 swap on face with unique ID: {face_id}') - else: - continue - # After the edge length loop, append the polygon to the manager for next time step. - manager.append(T1Swap, face_id=face_id, geom = geom, t1_threshold = t1_threshold, multiplier = multiplier, crit_area=crit_area) - + # First, we need to get the joint index over free and east edges, + # that is, the indices of edges that is spanning the entire graph without double edges + sheet.get_extra_indices() + edge_dataframe = sheet.edge_df.loc[sheet.sgle_edges] + short_edges = edge_dataframe.loc[(edge_dataframe['length'] < t1_threshold)] + # take advantage of the unique ID to help us tracking the edges, + # we need to reindex the df in type 1 transition function, otherwise there will be inconsistency between + # dataframes and will cause out of index error. + unique_list = short_edges['unique_id'].tolist() + for ID in unique_list: + idx = sheet.idx_lookup(ID,'edge') + print(f'Performed T1 swap on edge {idx}') + T1_transition(sheet, idx, do_reindex=True, remove_tri_faces=False, multiplier=multiplier) + manager.append(T1Swap, t1_threshold = t1_threshold, multiplier = multiplier) def T2Swap(sheet, manager, crit_area): From 2de4a80e23a04dbbcbf8274d7e8b6bd058d38f25 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:29:24 +1300 Subject: [PATCH 25/38] changed from individual cell based selection to conditional group selection --- .../behaviors/sheet/cell_class_events.py | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index fa3bae4b..f94088b2 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -8,7 +8,7 @@ from ...geometry.planar_geometry import PlanarGeometry from ...topology.sheet_topology import cell_division -def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): +def cell_cycle_transition(sheet, manager, dt, p_recruit=0.1, G2_duration=0.4, G1_duration=0.11): """ Controls cell class state transitions for cell cycle based on timers and probabilities. @@ -29,52 +29,38 @@ def cell_cycle_transition(sheet, manager, dt, face_id, p_recruit=0.1, G2_duratio G1_duration: float Fixed duration cells stay in G1 phase. """ - # First, we need to look up the current index of the face with face ID. - idx = sheet.idx_lookup(face_id, "face") - # Record the current cell class - current_class = sheet.face_df.loc[idx, 'cell_class'] - # (1) Recruit mature 'S' cells into G2 with probability p_recruit - if current_class == 'S': - if np.random.rand() < p_recruit: - sheet.face_df.loc[idx, 'cell_class'] = 'G2' - sheet.face_df.loc[idx, 'timer'] = G2_duration - # append to next deque - manager.append(cell_cycle_transition, dt=dt, face_id=face_id) - - # (2) Decrement timers for cells in G2; when timer ends, move to M - elif current_class == 'G2': - sheet.face_df.loc[idx, 'timer'] -= dt - if sheet.face_df.loc[idx, 'timer'] <= 0: - sheet.face_df.loc[idx, 'cell_class'] = 'M' - # append to next deque - manager.append(cell_cycle_transition, dt=dt, face_id=face_id) - - # (3) For cells in M, perform division and set daughters to G1 with timer - elif current_class == 'M': - # Make sure we pass the variable idx for cell division. - daughter = cell_division(sheet, mother=idx, geom = PlanarGeometry ) - # Set parent and daughter to G1 with G1 timer, note that variable daughter is the index of the new row already. - sheet.face_df.loc[idx, 'cell_class'] = 'G1' - sheet.face_df.loc[daughter, 'cell_class'] = 'G1' - sheet.face_df.loc[idx, 'timer'] = G1_duration - sheet.face_df.loc[daughter, 'timer'] = G1_duration - # append to next deque - manager.append(cell_cycle_transition, dt=dt, face_id=face_id) - # look up the unique id of daughter, then append. - daughter_id = sheet.face_df.loc[daughter, 'unique_id'] - print(f'daughter index {daughter} with unique ID: {daughter_id} is generated....') - manager.append(cell_cycle_transition, dt=dt, face_id=daughter_id) + # Generate two df that are cells that need to change its cell class or stay in its current class. + cells_stay_in_class = sheet.face_df.loc[sheet.face_df['timer'] > 0].index.tolist() + cells_to_change = sheet.face_df.loc[sheet.face_df['timer'] <= 0] - # (4) Decrement timers for G1 cells; when timer ends, move to S - elif current_class == 'G1': - sheet.face_df.loc[idx, 'timer'] -= dt - if sheet.face_df.loc[idx, 'timer'] <= 0: - sheet.face_df.loc[idx, 'cell_class'] = 'S' - # append to next deque - manager.append(cell_cycle_transition, dt=dt, face_id = face_id) + # For cells that still needs to elapse the timer, just reduce the timer by dt + for cell in cells_stay_in_class: + sheet.face_df.loc[cell,'timer'] -= dt + # Then we change the cell type based on the cell cycle diagram. + G1_cells = cells_to_change.loc[cells_to_change['cell_class'] == 'G1'].index.tolist() + S_cells = cells_to_change.loc[cells_to_change['cell_class'] == 'S'].index.tolist() + G2_cells = cells_to_change.loc[cells_to_change['cell_class'] == 'G2'].index.tolist() + M_cells = cells_to_change.loc[cells_to_change['cell_class'] == 'M'].index.tolist() + # For cells in G1_cells indices, we simply change the cell class to S + sheet.face_df.loc[G1_cells, 'cell_class'] = 'S' + # For cells in S_cells , we need to loop and decide based on the random number generated for if it moves into G2. + for cell in S_cells: + if np.random.rand() < p_recruit: + sheet.face_df.loc[cell, 'cell_class'] = 'G2' + sheet.face_df.loc[cell, 'timer'] = G2_duration + # For cells in G2_cells, we move them into M class + sheet.face_df.loc[G2_cells, 'cell_class'] = 'M' + # For cells in M_cells, they need to be undergone cell division + for cell in M_cells: + daughter = cell_division(sheet, mother=cell, geom = PlanarGeometry ) + # Set parent and daughter to G1 with G1 timer, note that variable daughter is the index of the new row already. + sheet.face_df.loc[cell, 'cell_class'] = 'G1' + sheet.face_df.loc[daughter, 'cell_class'] = 'G1' + sheet.face_df.loc[cell, 'timer'] = G1_duration + sheet.face_df.loc[daughter, 'timer'] = G1_duration From ca1ae788a324e55d4c5400a058999d3dde2cf815 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:09:33 +1300 Subject: [PATCH 26/38] use conditional selection in dataframes to perform cell cycle transitions --- src/tyssue/behaviors/sheet/cell_class_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tyssue/behaviors/sheet/cell_class_events.py b/src/tyssue/behaviors/sheet/cell_class_events.py index f94088b2..fb2bbe7a 100644 --- a/src/tyssue/behaviors/sheet/cell_class_events.py +++ b/src/tyssue/behaviors/sheet/cell_class_events.py @@ -64,3 +64,4 @@ def cell_cycle_transition(sheet, manager, dt, p_recruit=0.1, G2_duration=0.4, G1 sheet.face_df.loc[cell, 'timer'] = G1_duration sheet.face_df.loc[daughter, 'timer'] = G1_duration + manager.append(cell_cycle_transition, dt = dt, p_recruit = p_recruit,G2_duration = G2_duration, G1_duration = G1_duration) From c77c814c0c32ae8e13ae11ac589870318b0b1ff7 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 29 Oct 2025 18:18:35 +1300 Subject: [PATCH 27/38] baby version of proliferation behaviour function still needs to check how cell class is controlled in proliferation related actions --- .../behaviors/sheet/cell_activity_events.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/tyssue/behaviors/sheet/cell_activity_events.py diff --git a/src/tyssue/behaviors/sheet/cell_activity_events.py b/src/tyssue/behaviors/sheet/cell_activity_events.py new file mode 100644 index 00000000..1660de72 --- /dev/null +++ b/src/tyssue/behaviors/sheet/cell_activity_events.py @@ -0,0 +1,63 @@ +""" +This file contains all behaviour functions that models different cellular activities. + +""" + +import numpy as np +from ...geometry.planar_geometry import PlanarGeometry +from ...topology.sheet_topology import cell_division + +"""cell proliferation behaviour function + +A cell proliferation behaviour function does two thing on the selected cell. +First, it compares the current cell area with an area threshold to see if the cell large enough for division. +Secondly: if the cell is large enough, a cell division is performed on the cell; alternatively, if the cell area is +smaller than the threshold value, then the target area is added by "growth_rate * dt" to expand the cell. +""" + +# Note: still needs to improve the function, so it controls cell class change. +def proliferation(sheet, manager, geom, unique_id, crit_area, growth_rate, dt): + idx = sheet.idx_lookup('face', unique_id) + if sheet.face_df.loc[idx, "area"] > crit_area: + # restore prefered_area + sheet.face_df.loc[idx, "prefered_area"] = 1.0 + # Do division + daughter = cell_division(sheet, cell_id, geom) + # Update the topology + sheet.reset_index(order=True) + # update geometry + sgeom.update_all(sheet) + print(f"cell n°{daughter} is born") + else: + sheet.face_df.loc[idx, "prefered_area"] *= (1 + dt * growth_rate) + manager.append(division, geom = geom, unique_id = unique_id, crit_area = crit_area, growth_rate = growth_rate, dt = dt) + + + + +"""cell fusion behaviour function + +A cell fusion behaviour function is used when a CT is fusing into the STB layer. The cell class of the selection cell +should become "STB" at the end of the function. +First, the edges between the selected cell and its STB neighbours are disabled for edge tension term (coefficient = 0). +Secondly, we need to perform a T1 swap on the edge that connects a boundary vertex and the mutual vertex shared between all +STB neighbours and the fusing cell; in this way, the newly fused cell would have an edge that is "open" to "outside". +Thirdly, new dynamic parameters need to be updated to ensure consistent physics rule. +""" +def fusion(sheet, manager, geom, unique_id): + sheet.get_extra_indices() + idx = sheet.idx_lookup('face', unique_id) + # find the edge shared with STB units + + + +"""cell extrusion behaviour function + +A cell extrusion behaviour function has two components. +The first component is to let the STB to detach from the CT layer. This is done by a series of T1 transition on shared +edges between the selected STB and CTs. +The second component is to let the STB to shed from the layer. The shedding is modelled by cell removal but keep the +vertices shared between STB units that are still in the system. +""" + + From 098d083196531f5cb5759bfbb3635b73488a75f7 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:41:05 +1300 Subject: [PATCH 28/38] removed redundant print statement --- src/tyssue/behaviors/sheet/basic_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 88b389a3..2f878f51 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -278,7 +278,6 @@ def T2Swap(sheet, manager, crit_area): """ face_dataframe = sheet.face_df Face_list = face_dataframe.loc[(face_dataframe['num_sides'] < 4) & (face_dataframe['area'] < crit_area)].index.tolist() - print(f'Face list: {Face_list}') for face in Face_list: drop_face(sheet, face) manager.append(T2Swap, crit_area = crit_area) From dcefd6c38dbffa25511125e7211a44c93ab63150 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:41:44 +1300 Subject: [PATCH 29/38] created proliferation, fusion and extrusion cell activity behaviour function --- .../behaviors/sheet/cell_activity_events.py | 80 ++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/src/tyssue/behaviors/sheet/cell_activity_events.py b/src/tyssue/behaviors/sheet/cell_activity_events.py index 1660de72..b7485865 100644 --- a/src/tyssue/behaviors/sheet/cell_activity_events.py +++ b/src/tyssue/behaviors/sheet/cell_activity_events.py @@ -4,8 +4,9 @@ """ import numpy as np +import pandas as pd from ...geometry.planar_geometry import PlanarGeometry -from ...topology.sheet_topology import cell_division +from ...topology.sheet_topology import cell_division, type1_transition, remove_face """cell proliferation behaviour function @@ -14,7 +15,6 @@ Secondly: if the cell is large enough, a cell division is performed on the cell; alternatively, if the cell area is smaller than the threshold value, then the target area is added by "growth_rate * dt" to expand the cell. """ - # Note: still needs to improve the function, so it controls cell class change. def proliferation(sheet, manager, geom, unique_id, crit_area, growth_rate, dt): idx = sheet.idx_lookup('face', unique_id) @@ -33,31 +33,93 @@ def proliferation(sheet, manager, geom, unique_id, crit_area, growth_rate, dt): manager.append(division, geom = geom, unique_id = unique_id, crit_area = crit_area, growth_rate = growth_rate, dt = dt) - - """cell fusion behaviour function A cell fusion behaviour function is used when a CT is fusing into the STB layer. The cell class of the selection cell should become "STB" at the end of the function. First, the edges between the selected cell and its STB neighbours are disabled for edge tension term (coefficient = 0). + Secondly, we need to perform a T1 swap on the edge that connects a boundary vertex and the mutual vertex shared between all STB neighbours and the fusing cell; in this way, the newly fused cell would have an edge that is "open" to "outside". + Thirdly, new dynamic parameters need to be updated to ensure consistent physics rule. """ def fusion(sheet, manager, geom, unique_id): sheet.get_extra_indices() idx = sheet.idx_lookup('face', unique_id) - # find the edge shared with STB units - + # store the face index of STB neighbours + STB_neighbours = sheet.get_neighbors(idx) + STB_neighbours = list(STB_neighbours.intersection(set(sheet.face_df.loc[sheet.face_df['cell_class'] == 'STB'].index))) + # Find the edges associated with the fusing face index, filter out the boundary edges. + internal_edges = sheet.edge_df[(sheet.edge_df['face'] == idx) & (sheet.edge_df['opposite'] != -1)] + # We have to update both the internal arrowed edge and its opposite arrowed edge. + for ie in internal_edges.index: + sheet.edge_df.loc[ie,'is_active'] = 0 + sheet.edge_df.loc[sheet.edge_df.loc[ie,'opposite'],'is_active'] = 0 + + # Find all the boundary vertices in STB neighbours, based on the opposite == -1 value. + STB_boundary_edges = sheet.edge_df[ + (sheet.edge_df['face'].isin(STB_neighbours)) & + (sheet.edge_df['opposite'] == -1) + ] + STB_boundary_verts = pd.unique(STB_boundary_edges[['srce','trgt']].values.ravel()) + # Next, extract all the vertices belong to the fusing face, the edge connects a boundary vertex and the fusing face + # is the edge that we should perform T1 swap on. We can utilise the variable internal_edges. + fusing_face_verts = internal_edges['srce'] + # We only need to looping over the STB neighbours edges, then find edges connects a boundary vertex to a fusing face vertex. + STB_edges = sheet.edge_df[sheet.edge_df['face'].isin(STB_neighbours)] + matching_edge = STB_edges[ + ( + (STB_edges['srce'].isin(fusing_face_verts)) & (STB_edges['trgt'].isin(STB_boundary_verts)) + ) | + ( + (STB_edges['trgt'].isin(fusing_face_verts)) & (STB_edges['srce'].isin(STB_boundary_verts)) + ) + ] + if matching_edge.empty: + raise ValueError("No matching edge found between fusing face vertices and STB boundary vertices.") + first_edge_index = matching_edge.index[0] + New_boundary_edge = type1_transition(sheet,first_edge_index,do_reindex=False, remove_tri_faces=False, multiplier=1.5) + # Then make the new boundary edge to be active in tension (was dummy before T1). + sheet.edge_df.loc[New_boundary_edge,'is_active'] = 1 + geom.update_all(sheet) """cell extrusion behaviour function A cell extrusion behaviour function has two components. + The first component is to let the STB to detach from the CT layer. This is done by a series of T1 transition on shared -edges between the selected STB and CTs. +edges between the selected STB and CTs: Detach STB. + The second component is to let the STB to shed from the layer. The shedding is modelled by cell removal but keep the -vertices shared between STB units that are still in the system. +vertices shared between STB units that are still in the system: STB removal. """ +def extrude(sheet, manager, geom, unique_id): + idx = sheet.idx_lookup('face', unique_id) + remove.face(sheet,idx) + geom.update_all(sheet) - +def detach(sheet, manager, geom, unique_id): + sheet.get_extra_indices() + idx = sheet.idx_lookup('face', unique_id) + # Identify CT neighbours (non-STB) + CT_neighbours = sheet.get_neighbors(idx) + CT_neighbours = list(CT_neighbours.intersection( + set(sheet.face_df.loc[sheet.face_df['cell_class'] != 'STB'].index) + )) + # Find internal edges of the detaching face + internal_edges = sheet.edge_df[(sheet.edge_df['face'] == idx) & (sheet.edge_df['opposite'] != -1)] + # Filter internal edges that are shared with CT neighbours + shared_edges = internal_edges[ + sheet.edge_df.loc[internal_edges['opposite'], 'face'].isin(CT_neighbours).values + ] + if shared_edges.empty: + raise ValueError("No mutual edge found between detaching face and CT neighbours.") + # Perform T1 transitions on each shared edge and update geometry + for edge_idx in shared_edges.index: + new_edge_idx = type1_transition(sheet, edge_idx, do_reindex=False, remove_tri_faces=False, multiplier=1.5) + sheet.edge_df.loc[new_edge_idx, 'is_active'] = 1 + geom.update_all(sheet) + # Optionally extrude the detached face + manager.append(extrude, geom = geom, unique_id = unique_id) From 5e614449fa679f9d022b93f692b6883d574c65f2 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:37:10 +1300 Subject: [PATCH 30/38] bug fix for proliferation --- src/tyssue/behaviors/sheet/cell_activity_events.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tyssue/behaviors/sheet/cell_activity_events.py b/src/tyssue/behaviors/sheet/cell_activity_events.py index b7485865..f6be305e 100644 --- a/src/tyssue/behaviors/sheet/cell_activity_events.py +++ b/src/tyssue/behaviors/sheet/cell_activity_events.py @@ -17,20 +17,20 @@ """ # Note: still needs to improve the function, so it controls cell class change. def proliferation(sheet, manager, geom, unique_id, crit_area, growth_rate, dt): - idx = sheet.idx_lookup('face', unique_id) + idx = sheet.idx_lookup(unique_id,'face') # get the current face index from unique_id. if sheet.face_df.loc[idx, "area"] > crit_area: # restore prefered_area sheet.face_df.loc[idx, "prefered_area"] = 1.0 # Do division - daughter = cell_division(sheet, cell_id, geom) + daughter = cell_division(sheet, idx, geom) # Update the topology sheet.reset_index(order=True) # update geometry - sgeom.update_all(sheet) + geom.update_all(sheet) print(f"cell n°{daughter} is born") else: sheet.face_df.loc[idx, "prefered_area"] *= (1 + dt * growth_rate) - manager.append(division, geom = geom, unique_id = unique_id, crit_area = crit_area, growth_rate = growth_rate, dt = dt) + manager.append(proliferation, geom = geom, unique_id = unique_id, crit_area = crit_area, growth_rate = growth_rate, dt = dt) """cell fusion behaviour function From 13bf866e6f7d00decc6435d76d337e828bf14321 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:04:54 +1300 Subject: [PATCH 31/38] made create_gif compatible with windows --- src/tyssue/draw/plt_draw.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/tyssue/draw/plt_draw.py b/src/tyssue/draw/plt_draw.py index d48c3f0f..01c384ef 100644 --- a/src/tyssue/draw/plt_draw.py +++ b/src/tyssue/draw/plt_draw.py @@ -129,7 +129,39 @@ def create_gif( plt.close(fig) try: - subprocess.run(["convert", (graph_dir / "movie_*.png").as_posix(), output]) + # Expand pixels manually (cross-platform safe) + pngs = sorted(graph_dir.glob("movie_*.png")) + png_paths = [str(p) for p in pngs] + + # Detect the correct ImageMagick executable: + # IM6: convert + # IM7: magick convert + def find_imagemagick(): + # Try IM7 first + try: + subprocess.run(["magick", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return ["magick", "convert"] + except Exception: + pass + + # Try IM6 + try: + subprocess.run(["convert", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return ["convert"] + except Exception: + pass + + raise RuntimeError("ImageMagick executable not found on this system.") + + im_cmd = find_imagemagick() + + # Run ImageMagick safely + try: + subprocess.run(im_cmd + png_paths + [output], check=True) + except Exception as e: + print("Converting didn't work. Make sure ImageMagick is correctly installed.") + raise e + except Exception as e: print( "Converting didn't work, make sure imagemagick is available on your system" From 0f4f930dc73631f0a6f0c49cb130e8421611a8eb Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:01:10 +1300 Subject: [PATCH 32/38] renamed function to be delete_face to avoid using the same name from exisiting drop_face function in sheet_topology --- src/tyssue/topology/base_topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tyssue/topology/base_topology.py b/src/tyssue/topology/base_topology.py index 33312d49..3fd5ad50 100644 --- a/src/tyssue/topology/base_topology.py +++ b/src/tyssue/topology/base_topology.py @@ -183,7 +183,7 @@ def drop_two_sided_faces(eptm): eptm.face_df.drop(two_sided, axis=0, inplace=True) -def drop_face(sheet, face, **kwargs): +def delete_face(sheet, face, **kwargs): """ Removes the face indexed by "face" and all associated edges """ From a2dab4e3f8a6952785d06f442bc895f67eaf4c1b Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:06:24 +1300 Subject: [PATCH 33/38] revert rename --- src/tyssue/topology/base_topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tyssue/topology/base_topology.py b/src/tyssue/topology/base_topology.py index 3fd5ad50..33312d49 100644 --- a/src/tyssue/topology/base_topology.py +++ b/src/tyssue/topology/base_topology.py @@ -183,7 +183,7 @@ def drop_two_sided_faces(eptm): eptm.face_df.drop(two_sided, axis=0, inplace=True) -def delete_face(sheet, face, **kwargs): +def drop_face(sheet, face, **kwargs): """ Removes the face indexed by "face" and all associated edges """ From 0d47639c5892c65641975088a01a8947834b8dc3 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:21:41 +1300 Subject: [PATCH 34/38] created a function that updates drawing spec for bilayer model --- src/tyssue/draw/bilayer_drawing_tool.py | 40 +++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/tyssue/draw/bilayer_drawing_tool.py diff --git a/src/tyssue/draw/bilayer_drawing_tool.py b/src/tyssue/draw/bilayer_drawing_tool.py new file mode 100644 index 00000000..026d198c --- /dev/null +++ b/src/tyssue/draw/bilayer_drawing_tool.py @@ -0,0 +1,40 @@ +# This script contains a function that auto updates the drawing specifications for a bilayer tissue sheet. + +import numpy as np + +def bilayer_draw_spec_update(sheet, specs): + """ + Update drawing specifications for bilayer tissue sheet. + + Parameters + ---------- + sheet : tyssue.Sheet + The tissue sheet instance containing face_df and edge_df DataFrames. + specs : dict + The current drawing specifications dictionary to be updated. + """ + + # --- FACE COLOR UPDATE --- + # Use NumPy vectorization to assign colors: + # If 'cell_class' == 'STB', then color = 0.7 + # Else, then color = 0.1 + sheet.face_df['color'] = np.where( + sheet.face_df['cell_class'] == 'STB', 0.7, 0.1 + ) + + # Update the specs dictionary with the new face colors + specs['face']['color'] = sheet.face_df['color'] + + # Set transparency (alpha) for faces + specs['face']['alpha'] = 0.2 + + # --- EDGE WIDTH UPDATE --- + # Use NumPy vectorization to assign edge widths: + # If 'is_active' == 0, then width = 2 + # Else, then width = 0.5 + sheet.edge_df['width'] = np.where( + sheet.edge_df['is_active'] == 0, 2, 0.5 + ) + + # Update the specs dictionary with the new edge widths + specs['edge']['width'] = sheet.edge_df['width'] From ea99a2a9b725fd293db143f426b0216f1138896c Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:48:07 +1300 Subject: [PATCH 35/38] removes disconnected vertices after T1 --- src/tyssue/behaviors/sheet/basic_events.py | 6 ++++-- src/tyssue/behaviors/sheet/bilayer_dummy_set.py | 0 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 src/tyssue/behaviors/sheet/bilayer_dummy_set.py diff --git a/src/tyssue/behaviors/sheet/basic_events.py b/src/tyssue/behaviors/sheet/basic_events.py index 2f878f51..d472dd34 100644 --- a/src/tyssue/behaviors/sheet/basic_events.py +++ b/src/tyssue/behaviors/sheet/basic_events.py @@ -251,7 +251,7 @@ def contraction_line_tension(sheet, manager, **kwargs): ) -def T1Swap(sheet,manager, t1_threshold, multiplier): +def T1Swap(sheet,manager, geom, t1_threshold, multiplier): """ A behaviour function of the T1 transition that performs a T1 swap on an edge that is shorter than T1_threshold. """ @@ -268,7 +268,9 @@ def T1Swap(sheet,manager, t1_threshold, multiplier): idx = sheet.idx_lookup(ID,'edge') print(f'Performed T1 swap on edge {idx}') T1_transition(sheet, idx, do_reindex=True, remove_tri_faces=False, multiplier=multiplier) - manager.append(T1Swap, t1_threshold = t1_threshold, multiplier = multiplier) + sheet.reset_index() # removes disconnected vertices and faces + geom.update_all(sheet) + manager.append(T1Swap, geom= geom, t1_threshold = t1_threshold, multiplier = multiplier) def T2Swap(sheet, manager, crit_area): diff --git a/src/tyssue/behaviors/sheet/bilayer_dummy_set.py b/src/tyssue/behaviors/sheet/bilayer_dummy_set.py new file mode 100644 index 00000000..e69de29b From 1b7d7e0d781e5c15009360d8b4774876e208a423 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 26 Nov 2025 00:13:01 +1300 Subject: [PATCH 36/38] docstring improved --- .../behaviors/sheet/bilayer_dummy_set.py | 33 +++++++++++++++++++ src/tyssue/topology/base_topology.py | 6 +++- src/tyssue/topology/sheet_topology.py | 9 ++--- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/tyssue/behaviors/sheet/bilayer_dummy_set.py b/src/tyssue/behaviors/sheet/bilayer_dummy_set.py index e69de29b..6c3bf449 100644 --- a/src/tyssue/behaviors/sheet/bilayer_dummy_set.py +++ b/src/tyssue/behaviors/sheet/bilayer_dummy_set.py @@ -0,0 +1,33 @@ +""" The function in this script auto-controls the edges are active or not for a bilayer tissue sheet.""" + +def bilayer_dummy_set(sheet): + """ + Set edges as active or dummy for bilayer tissue sheet. + + Parameters + ---------- + sheet : tyssue.Sheet + The tissue sheet instance containing face_df and edge_df DataFrames. + """ + + # Iterate through each edge in the edge DataFrame + for i in sheet.edge_df.index: + # Check if the edge has an opposite edge (i.e., it's internal) + if sheet.edge_df.loc[i, 'opposite'] != -1: + # Get the associated cell (face) for this edge + associated_cell = sheet.edge_df.loc[i, 'face'] + # Get the opposite edge index + opposite_edge = sheet.edge_df.loc[i, 'opposite'] + # Get the opposite cell (face) for the opposite edge + opposite_cell = sheet.edge_df.loc[opposite_edge, 'face'] + # If both associated and opposite cells are of class 'STB', set edge as dummy (inactive) + if (sheet.face_df.loc[associated_cell, 'cell_class'] == 'STB' and + sheet.face_df.loc[opposite_cell, 'cell_class'] == 'STB'): + sheet.edge_df.loc[i, 'is_active'] = 0 + sheet.edge_df.loc[opposite_edge, 'is_active'] = 0 + else: + # Otherwise, set edge as active + sheet.edge_df.loc[i, 'is_active'] = 1 + else: + # For boundary edges, set as active + sheet.edge_df.loc[i, 'is_active'] = 1 diff --git a/src/tyssue/topology/base_topology.py b/src/tyssue/topology/base_topology.py index 33312d49..19f5e8cc 100644 --- a/src/tyssue/topology/base_topology.py +++ b/src/tyssue/topology/base_topology.py @@ -14,6 +14,7 @@ def split_vert(sheet, vert, face, to_rewire, epsilon, recenter=False): """Creates a new vertex and moves it towards the center of face. The edges in to_rewire will be connected to the new vertex. + This is the low-level function called by topologies when splitting vertices and rewiring edges. Parameters ---------- @@ -48,7 +49,10 @@ def split_vert(sheet, vert, face, to_rewire, epsilon, recenter=False): else: sheet.vert_df.loc[new_vert, sheet.coords] += shift - # rewire + # rewire the edges. + # Updates the rows in the original edge_df at the same indices as to_rewire. + # The replacement is done such that any occurrence of 'vert' in the 'srce' column or 'trgt' column of to_rewire + # is replaced with new_vert. sheet.edge_df.loc[to_rewire.index] = to_rewire.replace( {"srce": vert, "trgt": vert}, new_vert ) diff --git a/src/tyssue/topology/sheet_topology.py b/src/tyssue/topology/sheet_topology.py index 7b872ca7..2845f913 100644 --- a/src/tyssue/topology/sheet_topology.py +++ b/src/tyssue/topology/sheet_topology.py @@ -35,14 +35,15 @@ def split_vert( if face is None: face = np.random.choice(sheet.edge_df[sheet.edge_df["srce"] == vert]["face"]) - face_edges = sheet.edge_df.query(f"face == {face}") - (prev_v,) = face_edges[face_edges["trgt"] == vert]["srce"] - (next_v,) = face_edges[face_edges["srce"] == vert]["trgt"] + face_edges = sheet.edge_df.query(f"face == {face}") # A filerted view of the edge_df, contains only the edges belonging to the given face. + (prev_v,) = face_edges[face_edges["trgt"] == vert]["srce"] # The vertex connects into vert (the sources of the edges that whose target is vert). + (next_v,) = face_edges[face_edges["srce"] == vert]["trgt"] # The vertex connects out of vert (the targets of the edges whose source is vert). + # A filtered view of the edge_df, contains all edges that touch either prev_v or next_v, regardless of face. connected = sheet.edge_df[ sheet.edge_df["trgt"].isin((next_v, prev_v)) | sheet.edge_df["srce"].isin((next_v, prev_v)) ] - + # pass the filtered subset of edge_df as connected to rewire. base_split_vert(sheet, vert, face, connected, epsilon, recenter) new_edges = [] for face_ in connected["face"]: From 69529b5cb8ac0b070859b5427f28d845fa29b18e Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Wed, 26 Nov 2025 02:07:37 +1300 Subject: [PATCH 37/38] docstring improved --- src/tyssue/topology/base_topology.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tyssue/topology/base_topology.py b/src/tyssue/topology/base_topology.py index 19f5e8cc..efd023f0 100644 --- a/src/tyssue/topology/base_topology.py +++ b/src/tyssue/topology/base_topology.py @@ -148,23 +148,26 @@ def close_face(eptm, face): faces. Returns the index of the new edge if created, otherwise None """ logger.debug(f"closing face {face}") + # Collects all edges of the face. face_edges = eptm.edge_df[eptm.edge_df["face"] == face] srces = set(face_edges["srce"]) trgts = set(face_edges["trgt"]) - + # If the set of sources equals the set of targets, every vertex has both incoming and outgoing edges, then the loop is complete. if srces == trgts: logger.debug("Face %d already closed", face) return None try: - (single_srce,) = srces.difference(trgts) - (single_trgt,) = trgts.difference(srces) + (single_srce,) = srces.difference(trgts) # vertices that only appear as sources (no incoming edge). + (single_trgt,) = trgts.difference(srces) # vertices that only appear as targets (no outgoing edge). except ValueError as err: print("Closing only possible with exactly two dangling vertices") raise err - + # If there’s exactly one of each, the face is missing a single edge. + # Duplicates one existing edge row eptm.edge_df = pd.concat([eptm.edge_df, face_edges.iloc[0:1]], ignore_index=True) eptm.edge_df.index.name = "edge" new_edge = eptm.edge_df.index[-1] + # Reassigns its srce and trgt to connect the dangling vertices eptm.edge_df.loc[new_edge, ["srce", "trgt"]] = single_trgt, single_srce return new_edge From baccb5911f665a754f36b37abf3ff6d820b337d3 Mon Sep 17 00:00:00 2001 From: PinkGenji <63383516+PinkGenji@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:55:18 +1300 Subject: [PATCH 38/38] updates the specs dictionary with new face colours and new edge widths --- src/tyssue/draw/bilayer_drawing_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tyssue/draw/bilayer_drawing_tool.py b/src/tyssue/draw/bilayer_drawing_tool.py index 026d198c..c65589c3 100644 --- a/src/tyssue/draw/bilayer_drawing_tool.py +++ b/src/tyssue/draw/bilayer_drawing_tool.py @@ -23,7 +23,7 @@ def bilayer_draw_spec_update(sheet, specs): ) # Update the specs dictionary with the new face colors - specs['face']['color'] = sheet.face_df['color'] + specs['face']['color'] = sheet.face_df['color'].to_numpy() # Set transparency (alpha) for faces specs['face']['alpha'] = 0.2 @@ -37,4 +37,4 @@ def bilayer_draw_spec_update(sheet, specs): ) # Update the specs dictionary with the new edge widths - specs['edge']['width'] = sheet.edge_df['width'] + specs['edge']['width'] = sheet.edge_df['width'].to_numpy()