• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Pablo1990 / pyVertexModel / 11856264772

15 Nov 2024 12:28PM UTC coverage: 0.789%. Remained the same
11856264772

push

github

Pablo1990
displaying things differently

0 of 1008 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 1 file covered. (0.0%)

4 existing lines in 1 file now uncovered.

51 of 5452 relevant lines covered (0.94%)

0.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/pyVertexModel/algorithm/vertexModel.py
1
import logging
×
2
import os
×
3
from abc import abstractmethod
×
4
from itertools import combinations
×
5

6
import imageio
×
7
import numpy as np
×
8
import pandas as pd
×
9
import pyvista as pv
×
10
from scipy.stats import zscore
×
11
from skimage.measure import regionprops
×
12

13
from src.pyVertexModel.Kg.kg import add_noise_to_parameter
×
14
from src.pyVertexModel.algorithm import newtonRaphson
×
15
from src.pyVertexModel.geometry import degreesOfFreedom
×
16
from src.pyVertexModel.geometry.geo import Geo
×
17
from src.pyVertexModel.mesh_remodelling.remodelling import Remodelling
×
18
from src.pyVertexModel.parameters.set import Set
×
19
from src.pyVertexModel.util.utils import save_state, save_backup_vars, load_backup_vars, copy_non_mutable_attributes
×
20

21
logger = logging.getLogger("pyVertexModel")
×
22

23

24
def screenshot(v_model, temp_dir, selected_cells=None):
×
25
    """
26
    Create a screenshot of the current state of the model.
27
    :param v_model: VertexModel object.
28
    :param selected_cells: List of selected cells to be displayed.
29
    :param temp_dir: Temporary directory to save the images.
30
    :return:
31
    """
32
    # if exists variable export_images in set
33
    if hasattr(v_model.set, 'export_images'):
×
34
        if v_model.set.export_images is False:
×
35
            return
×
36

37
    total_real_cells = len([cell.ID for cell in v_model.geo.Cells if cell.AliveStatus is not None])
×
38

39
    # Create a colormap_lim
40
    #if v_model.colormap_lim is None:
41
    #    v_model.colormap_lim = [0, total_real_cells]
42

43
    # Create a plotter
44
    if selected_cells is None:
×
45
        selected_cells = []
×
46

47
    plotter = pv.Plotter(off_screen=True)
×
48

49
    for _, cell in enumerate(v_model.geo.Cells):
×
50
        if cell.AliveStatus == 1 and (cell.ID in selected_cells or selected_cells is not []):
×
51
            # Load the VTK file as a pyvista mesh
52
            mesh = cell.create_pyvista_mesh()
×
53

54
            # Add the mesh to the plotter
55
            # Cmaps that I like: 'tab20b', 'BuPu', 'Blues'
56
            # Cmaps that I don't like: 'prism', 'bone'
NEW
57
            plotter.add_mesh(mesh, name=f'cell_{cell.ID}', scalars='Volume', lighting=True, cmap="pink",
×
58
                             clim=[0.0001, 0.0006], show_edges=True, edge_color='white', edge_opacity=0.3)
59

60

61
    for _, cell in enumerate(v_model.geo.Cells):
×
62
        if cell.AliveStatus == 1 and (cell.ID in selected_cells or selected_cells is not []):
×
63
            edge_mesh = cell.create_pyvista_edges()
×
NEW
64
            plotter.add_mesh(edge_mesh, name=f'edge_{cell.ID}', color='black', line_width=3, render_lines_as_tubes=True)
×
65

66

67

68
    # Set a fixed camera zoom level
69
    fixed_zoom_level = 1
×
70
    plotter.camera.zoom(fixed_zoom_level)
×
71

72
    # Add text to the plotter
73
    if v_model.set.ablation:
×
74
        timeAfterAblation = float(v_model.t) - float(v_model.set.TInitAblation)
×
75
        text_content = f"Ablation time: {timeAfterAblation:.2f}"
×
76
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
77
    else:
78
        text_content = f"Time: {v_model.t:.2f}"
×
79
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
80

81

82

83
    # Render the scene and capture a screenshot
84
    img = plotter.screenshot(transparent_background=True, scale=3)
×
85
    # Save the image to a temporary file
86
    temp_file = os.path.join(temp_dir, f'vModel_perspective_{v_model.numStep}.png')
×
87
    imageio.imwrite(temp_file, img)
×
88

89
    # True 2D
90
    plotter.enable_parallel_projection()
×
91
    plotter.enable_image_style()
×
92

93
    # Set the camera to the top view
94
    plotter.view_xy()
×
95

96
    img = plotter.screenshot(transparent_background=True, scale=3)
×
97
    temp_file = os.path.join(temp_dir, f'vModel_top_{v_model.numStep}.png')
×
98
    imageio.imwrite(temp_file, img)
×
99

100
    # Set the camera to the bottom view
101
    plotter.view_xy(negative=True)
×
102

103
    img = plotter.screenshot(transparent_background=True, scale=3)
×
104
    temp_file = os.path.join(temp_dir, f'vModel_bottom_{v_model.numStep}.png')
×
105
    imageio.imwrite(temp_file, img)
×
106

107
    # Set the camera to the front view and adjust the position to be inside the tissue
108
    plotter.view_xz()
×
109
    #center_x, center_y, center_z = v_model.geo.Cells[0].X
110
    plotter.camera.position = (-0.6836302475532527, 0.49746550619602203, -0.0024260058999061584)
×
111
    plotter.focal_point = (0.5723095089197159, 0.49746550619602203, -0.0024260058999061584)
×
112
    # Do not show the following cells =
113
    cells_to_hide = np.array([11, 12, 16, 19, 21, 22, 23, 31, 32, 35, 36, 37, 38, 39, 41, 44, 54, 55, 56, 58, 59, 60, 61, 65, 67, 68, 75, 76, 81, 82, 83, 86, 88, 89, 90, 91, 94, 96, 97, 102, 104, 106, 108, 112, 114, 116, 119, 120, 123, 124, 128, 129, 133, 135, 140, 141, 142, 143, 144, 145, 149])
×
114
    for cell in cells_to_hide:
×
115
        plotter.remove_actor(f'cell_{cell}')
×
116
        plotter.remove_actor(f'edge_{cell}')
×
117

118
    img = plotter.screenshot(transparent_background=True, scale=3)
×
119
    temp_file = os.path.join(temp_dir, f'vModel_front_{v_model.numStep}.png')
×
120
    imageio.imwrite(temp_file, img)
×
121

122
    # Close the plotter
123
    plotter.close()
×
124

125
def generate_tetrahedra_from_information(X, cell_edges, cell_height, cell_centroids, main_cells,
×
126
                                         neighbours_network, selected_planes, triangles_connectivity,
127
                                         vertices_of_cell_pos, geo):
128
    """
129
    Generate tetrahedra from the information of the cells.
130
    :param X:
131
    :param cell_edges:
132
    :param cell_height:
133
    :param cell_centroids:
134
    :param main_cells:
135
    :param neighbours_network:
136
    :param selected_planes:
137
    :param triangles_connectivity:
138
    :param vertices_of_cell_pos:
139
    :param geo:
140
    :return:
141
    """
142
    bottom_plane = 0
×
143
    top_plane = 1
×
144
    if bottom_plane == 0:
×
145
        z_coordinate = [-cell_height, cell_height]
×
146
    else:
147
        z_coordinate = [cell_height, -cell_height]
×
148
    Twg = []
×
149
    for idPlane, numPlane in enumerate(selected_planes):
×
150
        # Using the centroids and vertices of the cells of each 2D image as ghost nodes
151
        X, Xg_faceIds, Xg_ids, Xg_verticesIds = (
×
152
            add_faces_and_vertices_to_x(X,
153
                                        np.hstack((cell_centroids[numPlane][:, 1:3], np.tile(z_coordinate[idPlane],
154
                                                                                             (len(cell_centroids[
155
                                                                                                      numPlane][:,
156
                                                                                                  1:3]), 1)))),
157
                                        np.hstack((np.fliplr(vertices_of_cell_pos[numPlane]),
158
                                                   np.tile(z_coordinate[idPlane],
159
                                                           (len(vertices_of_cell_pos[numPlane]), 1))))))
160

161
        # Fill Geo info
162
        if idPlane == bottom_plane:
×
163
            geo.XgBottom = Xg_ids
×
164
        elif idPlane == top_plane:
×
165
            geo.XgTop = Xg_ids
×
166

167
        # Create tetrahedra
168
        Twg_numPlane = create_tetrahedra(triangles_connectivity[numPlane], neighbours_network[numPlane],
×
169
                                         cell_edges[numPlane], main_cells, Xg_faceIds, Xg_verticesIds)
170

171
        Twg.append(Twg_numPlane)
×
172
    Twg = np.vstack(Twg)
×
173
    return Twg, X
×
174

175

176
def add_faces_and_vertices_to_x(X, Xg_faceCentres2D, Xg_vertices2D):
×
177
    """
178
    Add faces and vertices to the X matrix.
179
    :param X:
180
    :param Xg_faceCentres2D:
181
    :param Xg_vertices2D:
182
    :return:
183
    """
184
    Xg_nodes = np.vstack((Xg_faceCentres2D, Xg_vertices2D))
×
185
    Xg_ids = np.arange(X.shape[0] + 1, X.shape[0] + Xg_nodes.shape[0] + 1)
×
186
    Xg_faceIds = Xg_ids[0:Xg_faceCentres2D.shape[0]]
×
187
    Xg_verticesIds = Xg_ids[Xg_faceCentres2D.shape[0]:]
×
188
    X = np.vstack((X, Xg_nodes))
×
189
    return X, Xg_faceIds, Xg_ids, Xg_verticesIds
×
190

191

192
def create_tetrahedra(triangles_connectivity, neighbours_network, edges_of_vertices, x_internal, x_face_ids,
×
193
                      x_vertices_ids):
194
    """
195
    Add connections between real nodes and ghost cells to create tetrahedra.
196

197
    :param triangles_connectivity: A 2D array where each row represents a triangle connectivity.
198
    :param neighbours_network: A 2D array where each row represents a pair of neighboring nodes.
199
    :param edges_of_vertices: A list of lists where each sublist represents the edges of a vertex.
200
    :param x_internal: A 1D array representing the internal nodes.
201
    :param x_face_ids: A 1D array representing the face ids.
202
    :param x_vertices_ids: A 1D array representing the vertices ids.
203
    :return: A 2D array representing the tetrahedra.
204
    """
205
    x_ids = np.concatenate([x_face_ids, x_vertices_ids])
×
206

207
    # Relationships: 1 ghost node, three cell nodes
208
    twg = np.hstack([triangles_connectivity, x_vertices_ids[:, None]])
×
209

210
    # Relationships: 1 cell node and 3 ghost nodes
211
    new_additions = []
×
212
    for id_cell, num_cell in enumerate(x_internal):
×
213
        face_id = x_face_ids[id_cell]
×
214
        vertices_to_connect = edges_of_vertices[id_cell]
×
215
        new_additions.extend(np.hstack([np.repeat(np.array([[num_cell, face_id]]), len(vertices_to_connect), axis=0),
×
216
                                        x_vertices_ids[vertices_to_connect]]))
217
    twg = np.vstack([twg, new_additions])
×
218

219
    # Relationships: 2 ghost nodes, two cell nodes
220
    twg_sorted = np.sort(twg[np.any(np.isin(twg, x_ids), axis=1)], axis=1)
×
221
    internal_neighbour_network = [neighbour for neighbour in neighbours_network if
×
222
                                  np.any(np.isin(neighbour, x_internal))]
223
    internal_neighbour_network = np.unique(np.sort(internal_neighbour_network, axis=1), axis=0)
×
224

225
    new_additions = []
×
226
    for num_pair in range(internal_neighbour_network.shape[0]):
×
227
        found = np.isin(twg_sorted, internal_neighbour_network[num_pair])
×
228
        new_connections = np.unique(twg_sorted[np.sum(found, axis=1) == 2, 3])
×
229
        if len(new_connections) > 1:
×
230
            new_connections_pairs = np.array(list(combinations(new_connections, 2)))
×
231
            new_additions.extend([np.hstack([internal_neighbour_network[num_pair], new_connections_pair])
×
232
                                  for new_connections_pair in new_connections_pairs])
233
        else:
234
            raise ValueError('Error while creating the connections and initial topology')
×
235
    twg = np.vstack([twg, new_additions])
×
236

237
    return twg
×
238

239

240
def calculate_cell_height_on_model(img2DLabelled, main_cells, c_set):
×
241
    """
242
    Calculate the cell height on the model regarding the diameter of the cells.
243
    :param img2DLabelled:
244
    :param main_cells:
245
    :return:
246
    """
247
    properties = regionprops(img2DLabelled)
×
248
    # Extract major axis lengths
249
    avg_diameter = np.mean([prop.major_axis_length for prop in properties if prop.label in main_cells])
×
250
    cell_height = avg_diameter * c_set.CellHeight
×
251
    return cell_height
×
252

253

254
class VertexModel:
×
255
    """
256
    The main class for the vertex model simulation. It contains the methods for initializing the model,
257
    iterating over time, applying Brownian motion, and checking the integrity of the model.
258
    """
259

260
    def __init__(self, c_set=None, create_output_folder=True, update_derived_parameters=True):
×
261
        """
262
        Vertex Model class.
263
        :param c_set:
264
        """
265
        self.colormap_lim = None
×
266
        self.OutputFolder = None
×
267
        self.numStep = None
×
268
        self.backupVars = None
×
269
        self.geo_n = None
×
270
        self.geo_0 = None
×
271
        self.tr = None
×
272
        self.t = None
×
273
        self.X = None
×
274
        self.didNotConverge = False
×
275
        self.geo = Geo()
×
276

277
        # Set definition
278
        if c_set is not None:
×
279
            self.set = c_set
×
280
        else:
281
            # TODO Create a menu to select the set
282
            self.set = Set()
×
283
            # self.set.cyst()
284
            self.set.wing_disc()
×
285
            if self.set.ablation:
×
286
                self.set.wound_default()
×
287

288
            if update_derived_parameters:
×
289
                self.set.update_derived_parameters()
×
290

291
        # Redirect output
292
        if self.set.OutputFolder is not None and create_output_folder:
×
293
            self.set.redirect_output()
×
294

295
        # Degrees of freedom definition
296
        self.Dofs = degreesOfFreedom.DegreesOfFreedom()
×
297

298
        self.relaxingNu = False
×
299
        self.EnergiesPerTimeStep = []
×
300
        self.t = 0
×
301
        self.tr = 0
×
302
        self.numStep = 1
×
303

304
    @abstractmethod
×
305
    def initialize(self):
×
306
        pass
×
307

308
    def brownian_motion(self, scale):
×
309
        """
310
        Applies Brownian motion to the vertices of cells in the Geo structure.
311
        Displacements are generated with a normal distribution in each dimension.
312
        :param scale:
313
        :return:
314
        """
315

316
        # Concatenate and sort all tetrahedron vertices
317
        all_tets = np.sort(np.vstack([cell.T for cell in self.geo.Cells if cell.AliveStatus is not None]), axis=1)
×
318
        all_tets_unique = np.unique(all_tets, axis=0)
×
319

320
        # Generate random displacements with a normal distribution for each dimension
321
        displacements = (scale * (np.linalg.norm(self.geo.Cells[14].X - self.geo.Cells[15].X)) *
×
322
                         np.random.randn(all_tets_unique.shape[0], 3))
323

324
        # Update vertex positions based on 3D Brownian motion displacements
325
        for cell in [c for c in self.geo.Cells if c.AliveStatus is not None and c.ID not in self.geo.BorderCells]:
×
326
            _, corresponding_ids = np.where(np.all(np.sort(cell.T, axis=1)[:, None] == all_tets_unique, axis=2))
×
327
            cell.Y += displacements[corresponding_ids, :]
×
328

329
    def iterate_over_time(self):
×
330
        """
331
        Iterate the model over time. This includes updating the degrees of freedom, applying boundary conditions,
332
        updating measures, and checking for convergence.
333
        :return:
334
        """
335
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
336
        if not os.path.exists(temp_dir):
×
337
            os.makedirs(temp_dir)
×
338

339
        if self.set.Substrate == 1:
×
340
            self.Dofs.GetDOFsSubstrate(self.geo, self.set)
×
341
        else:
342
            self.Dofs.get_dofs(self.geo, self.set)
×
343

344
        self.geo.remodelling = False
×
345
        if self.geo_0 is None:
×
346
            self.geo_0 = self.geo.copy(update_measurements=False)
×
347

348
        if self.geo_n is None:
×
349
            self.geo_n = self.geo.copy(update_measurements=False)
×
350

351
        self.backupVars = save_backup_vars(self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs)
×
352

353
        print("File: ", self.set.OutputFolder)
×
354
        self.save_v_model_state()
×
355

356
        while self.t <= self.set.tend and not self.didNotConverge:
×
357
            gr = self.single_iteration()
×
358

359
            if np.isnan(gr):
×
360
                break
×
361

362
        return self.didNotConverge
×
363

364
    def single_iteration(self, post_operations=True):
×
365
        """
366
        Perform a single iteration of the model.
367
        :return:
368
        """
369
        self.set.currentT = self.t
×
370
        logger.info("Time: " + str(self.t))
×
371
        if not self.relaxingNu:
×
372
            self.set.i_incr = self.numStep
×
373

374
            # Ablate cells if needed
375
            if self.set.ablation:
×
376
                if self.set.ablation and self.set.TInitAblation <= self.t and self.geo.cellsToAblate is not None:
×
377
                    self.save_v_model_state(file_name='before_ablation')
×
378
                self.geo.ablate_cells(self.set, self.t)
×
379
                self.geo_n = self.geo.copy()
×
380
                # Update the degrees of freedom
381
                self.Dofs.get_dofs(self.geo, self.set)
×
382

383
            self.Dofs.ApplyBoundaryCondition(self.t, self.geo, self.set)
×
384
            # IMPORTANT: Here it updates: Areas, Volumes, etc... Should be
385
            # up-to-date
386
            self.geo.update_measures()
×
387
        if self.set.implicit_method is True:
×
388
            g, K, _, energies = newtonRaphson.KgGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
389
                                                       self.set.implicit_method)
390
        else:
391
            K = 0
×
392
            g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
393
                                                self.set.implicit_method)
394
        for key, energy in energies.items():
×
395
            logger.info(f"{key}: {energy}")
×
396
        self.geo, g, __, __, self.set, gr, dyr, dy = newtonRaphson.newton_raphson(self.geo_0, self.geo_n, self.geo,
×
397
                                                                                  self.Dofs, self.set, K, g,
398
                                                                                  self.numStep, self.t,
399
                                                                                  self.set.implicit_method)
400
        if not np.isnan(gr) and post_operations:
×
401
            self.post_newton_raphson(dy, dyr, g, gr)
×
402
        return gr
×
403

404
    def post_newton_raphson(self, dy, dyr, g, gr):
×
405
        """
406
        Post Newton Raphson operations.
407
        :param dy:
408
        :param dyr:
409
        :param g:
410
        :param gr:
411
        :return:
412
        """
413
        if (gr < self.set.tol and dyr < self.set.tol and np.all(~np.isnan(g[self.Dofs.Free])) and
×
414
                np.all(~np.isnan(dy[self.Dofs.Free]))):
415
            self.iteration_converged()
×
416
            # if self.set.implicit_method is False:
417
            #     self.set.tol = gr
418
            #     if self.set.tol < self.set.tol0:
419
            #         self.set.tol = self.set.tol0
420
        else:
421
            self.iteration_did_not_converged()
×
422

423
        self.Dofs.get_dofs(self.geo, self.set)
×
424

425
    def iteration_did_not_converged(self):
×
426
        """
427
        If the iteration did not converge, the algorithm will try to relax the value of nu and dt.
428
        :return:
429
        """
430
        self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs = load_backup_vars(self.backupVars)
×
431
        self.relaxingNu = False
×
432
        if self.set.iter == self.set.MaxIter0 and self.set.implicit_method:
×
433
            self.set.MaxIter = self.set.MaxIter0 * 3
×
434
            self.set.nu = 10 * self.set.nu0
×
435
        else:
436
            if (self.set.iter >= self.set.MaxIter and
×
437
                    (self.set.dt / self.set.dt0) > 1e-6):
438
                self.set.MaxIter = self.set.MaxIter0
×
439
                self.set.nu = self.set.nu0
×
440
                self.set.dt = self.set.dt / 2
×
441
                self.t = self.set.last_t_converged + self.set.dt
×
442
            else:
443
                self.didNotConverge = True
×
444

445
    def iteration_converged(self):
×
446
        """
447
        If the iteration converged, the algorithm will update the values of the variables and proceed to the next step.
448
        :return:
449
        """
450
        if self.set.nu / self.set.nu0 == 1:
×
451
            # STEP has converged
452
            logger.info(f"STEP {str(self.set.i_incr)} has converged ...")
×
453

454
            # Build X From Y
455
            self.geo.build_x_from_y(self.geo_n)
×
456

457
            # Remodelling
458
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
459
                if self.set.Remodelling:
×
460
                    save_state(self,
×
461
                               os.path.join(self.set.OutputFolder,
462
                                            'data_step_before_remodelling_' + str(self.numStep) + '.pkl'))
463

464
                    # Remodelling
465
                    remodel_obj = Remodelling(self.geo, self.geo_n, self.geo_0, self.set, self.Dofs)
×
466
                    self.geo, self.geo_n = remodel_obj.remodel_mesh(self.numStep)
×
467
                    # Update tolerance if remodelling was performed to the current one
468
                    if self.set.implicit_method is False:
×
469
                        g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
470
                                                            self.set.implicit_method)
471
                        self.Dofs.get_dofs(self.geo, self.set)
×
472
                        gr = np.linalg.norm(g[self.Dofs.Free])
×
473

474
            # Update last time converged
475
            self.set.last_t_converged = self.t
×
476

477
            # Test Geo
478
            # TODO: CHECK
479
            # self.check_integrity()
480

481
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
482
                self.save_v_model_state()
×
483

484
                # Reset noise to be comparable between simulations
485
                self.reset_noisy_parameters()
×
486
                # Count the number of faces in average has a cell per domain
487
                self.geo.update_barrier_tri0_based_on_number_of_faces()
×
488
                self.tr = self.t
×
489

490
                # Brownian Motion
491
                if self.set.brownian_motion is True:
×
492
                    self.brownian_motion(self.set.brownian_motion_scale)
×
493

494
            self.t = self.t + self.set.dt
×
495
            self.set.dt = np.min([self.set.dt + self.set.dt * 0.5, self.set.dt0])
×
496
            self.set.MaxIter = self.set.MaxIter0
×
497
            self.numStep = self.numStep + 1
×
498
            self.backupVars = {
×
499
                'Geo_b': self.geo.copy(),
500
                'Geo_n_b': self.geo_n.copy(),
501
                'Geo_0_b': self.geo_0.copy(),
502
                'tr_b': self.tr,
503
                'Dofs': self.Dofs.copy()
504
            }
505
            self.geo_n = self.geo.copy()
×
506
            self.relaxingNu = False
×
507
        else:
508
            self.set.nu = np.max([self.set.nu / 2, self.set.nu0])
×
509
            self.relaxingNu = True
×
510

511
    def save_v_model_state(self, file_name=None):
×
512
        """
513
        Save the state of the vertex model.
514
        :param file_name:
515
        :return:
516
        """
517
        # Create VTK files for the current state
518
        self.geo.create_vtk_cell(self.set, self.numStep, 'Edges')
×
519
        self.geo.create_vtk_cell(self.set, self.numStep, 'Cells')
×
520
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
521
        screenshot(self, temp_dir)
×
522
        # Save Data of the current step
523
        if file_name is None:
×
524
            save_state(self, os.path.join(self.set.OutputFolder, 'data_step_' + str(self.numStep) + '.pkl'))
×
525
        else:
526
            save_state(self, os.path.join(self.set.OutputFolder, file_name + '.pkl'))
×
527

528
    def reset_noisy_parameters(self):
×
529
        """
530
        Reset noisy parameters.
531
        :return:
532
        """
533
        for cell in self.geo.Cells:
×
534
            cell.lambda_s1_perc = add_noise_to_parameter(1, self.set.noise_random)
×
535
            cell.lambda_s2_perc = add_noise_to_parameter(1, self.set.noise_random)
×
536
            cell.lambda_s3_perc = add_noise_to_parameter(1, self.set.noise_random)
×
537
            cell.lambda_v_perc = add_noise_to_parameter(1, self.set.noise_random)
×
538
            cell.lambda_r_perc = add_noise_to_parameter(1, self.set.noise_random)
×
539
            cell.c_line_tension_perc = add_noise_to_parameter(1, self.set.noise_random)
×
540
            cell.k_substrate_perc = add_noise_to_parameter(1, self.set.noise_random)
×
541
            cell.lambda_b_perc = add_noise_to_parameter(1, self.set.noise_random)
×
542

543
    def check_integrity(self):
×
544
        """
545
        Performs tests on the properties of cells, faces, and triangles (tris) within the Geo structure.
546
        Ensures that certain geometrical properties are above minimal threshold values.
547
        """
548

549
        # Define minimum error thresholds for edge length, area, and volume
550
        min_error_edge = 1e-5
×
551
        min_error_area = min_error_edge ** 2
×
552
        min_error_volume = min_error_edge ** 3
×
553

554
        # Test Cells properties:
555
        # Conditions checked:
556
        # - Volume > minimum error volume
557
        # - Initial Volume > minimum error volume
558
        # - Area > minimum error area
559
        # - Initial Area > minimum error area
560
        for c_cell in self.geo.Cells:
×
561
            if c_cell.AliveStatus:
×
562
                assert c_cell.Vol > min_error_volume, "Cell volume is too low"
×
563
                assert c_cell.Vol0 > min_error_volume, "Cell initial volume is too low"
×
564
                assert c_cell.Area > min_error_area, "Cell area is too low"
×
565
                assert c_cell.Area0 > min_error_area, "Cell initial area is too low"
×
566

567
        # Test Faces properties:
568
        # Conditions checked:
569
        # - Area > minimum error area
570
        # - Initial Area > minimum error area
571
        for c_cell in self.geo.Cells:
×
572
            if c_cell.AliveStatus:
×
573
                for face in c_cell.Faces:
×
574
                    assert face.Area > min_error_area, "Face area is too low"
×
575
                    assert face.Area0 > min_error_area, "Face initial area is too low"
×
576

577
        # Test Tris properties:
578
        # Conditions checked:
579
        # - Edge length > minimum error edge length
580
        # - Any Lengths to Centre > minimum error edge length
581
        # - Area > minimum error area
582
        for c_cell in self.geo.Cells:
×
583
            if c_cell.AliveStatus:
×
584
                for face in c_cell.Faces:
×
585
                    for tris in face.Tris:
×
586
                        assert tris.EdgeLength > min_error_edge, "Triangle edge length is too low"
×
587
                        assert any(length > min_error_edge for length in
×
588
                                   tris.LengthsToCentre), "Triangle lengths to centre are too low"
589
                        assert tris.Area > min_error_area, "Triangle area is too low"
×
590

591
    def analyse_vertex_model(self):
×
592
        """
593
        Analyse the vertex model.
594
        :return:
595
        """
596
        # Initialize average cell properties
597
        cell_features = []
×
598
        debris_features = []
×
599

600
        wound_centre, debris_cells = self.geo.compute_wound_centre()
×
601
        list_of_cell_distances = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=None)
×
602
        list_of_cell_distances_top = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=0)
×
603
        list_of_cell_distances_bottom = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=2)
×
604

605
        # Analyse the alive cells
606
        for cell_id, cell in enumerate(self.geo.Cells):
×
607
            if cell.AliveStatus:
×
608
                cell_features.append(cell.compute_features(wound_centre))
×
609
            elif cell.AliveStatus is not None:
×
610
                debris_features.append(cell.compute_features())
×
611

612
        # Calculate average of cell features
613
        all_cell_features = pd.DataFrame(cell_features)
×
614
        all_cell_features["cell_distance_to_wound"] = list_of_cell_distances
×
615
        all_cell_features["cell_distance_to_wound_top"] = list_of_cell_distances_top
×
616
        all_cell_features["cell_distance_to_wound_bottom"] = list_of_cell_distances_bottom
×
617
        all_cell_features["time"] = self.t
×
618
        avg_cell_features = all_cell_features.mean()
×
619

620
        # Compute wound features
621
        try:
×
622
            wound_features = self.compute_wound_features()
×
623
            avg_cell_features = pd.concat([avg_cell_features, pd.Series(wound_features)])
×
624
        except Exception as e:
×
625
            pass
×
626

627
        return all_cell_features, avg_cell_features
×
628

629
    def compute_wound_features(self):
×
630
        """
631
        Compute wound features.
632
        :return:
633
        """
634
        wound_features = {
×
635
            'num_cells_wound_edge': len(self.geo.compute_cells_wound_edge()),
636
            'num_cells_wound_edge_top': len(self.geo.compute_cells_wound_edge(location_filter="Top")),
637
            'num_cells_wound_edge_bottom': len(self.geo.compute_cells_wound_edge(location_filter="Bottom")),
638
            'wound_area_top': self.geo.compute_wound_area(location_filter="Top"),
639
            'wound_area_bottom': self.geo.compute_wound_area(location_filter="Bottom"),
640
            'wound_volume': self.geo.compute_wound_volume(),
641
            'wound_height': self.geo.compute_wound_height(),
642
            'wound_aspect_ratio_top': self.geo.compute_wound_aspect_ratio(location_filter="Top"),
643
            'wound_aspect_ratio_bottom': self.geo.compute_wound_aspect_ratio(location_filter="Bottom"),
644
            'wound_perimeter_top': self.geo.compute_wound_perimeter(location_filter="Top"),
645
            'wound_perimeter_bottom': self.geo.compute_wound_perimeter(location_filter="Bottom")
646
        }
647

648
        return wound_features
×
649

650
    def copy(self):
×
651
        """
652
        Copy the VertexModel object.
653
        :return:
654
        """
655
        new_v_model = VertexModel()
×
656
        copy_non_mutable_attributes(self, '', new_v_model)
×
657

658
        return new_v_model
×
659

660
    def calculate_error(self, K, initial_recoil, error_type=None):
×
661
        """
662
        Calculate the error of the model.
663
        :return:
664
        """
665
        # The error consist on:
666
        # - There shouldn't be any cells with very small area in the top or bottom domain.
667
        # - It should get until the end of the simulation (tend).
668
        # - When ablating, it should get to: 165 percentage of area at 35.8 minutes.
669
        error = 0
×
670

671
        # Check if the simulation reached the end
672
        if self.t < self.set.tend:
×
673
            error += (self.t - self.set.tend) ** 2
×
674

675
        # # Check how many cells have a very small area
676
        if error_type == 'None' or 'SmallArea' in error_type:
×
677
            std_area_top = np.std([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
678
            std_area_bottom = np.std([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
679
            mean_area_top = np.mean([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
680
            mean_area_bottom = np.mean([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
681
            zscore_area_top = std_area_top / mean_area_top
×
682
            zscore_area_bottom = std_area_bottom / mean_area_bottom
×
683
            error += zscore_area_top ** 2
×
684
            error += zscore_area_bottom ** 2
×
685

686
        # Check how similar the recoil from in vivo is to the initial recoil and K value
687
        correct_K = 0.126
×
688
        correct_initial_recoil = 0.213
×
689
        if error_type == 'None' or 'K' in error_type:
×
690
            try:
×
691
                error += np.abs(K[0] - correct_K) * 100
×
692
            except IndexError:
×
693
                error += np.abs(K - correct_K) * 100
×
694

695
        if error_type == 'None' or 'InitialRecoil' in error_type:
×
696
            try:
×
697
                error += np.abs(initial_recoil[0] - correct_initial_recoil) * 100
×
698
            except IndexError:
×
699
                error += np.abs(initial_recoil - correct_initial_recoil) * 100
×
700

701
        return error
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc