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

Pablo1990 / pyVertexModel / 11438761029

21 Oct 2024 11:04AM UTC coverage: 0.796%. Remained the same
11438761029

push

github

Pablo1990
bugfix on border cells

0 of 997 branches covered (0.0%)

Branch coverage included in aggregate %.

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

154 existing lines in 2 files now uncovered.

51 of 5414 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.algorithm import newtonRaphson
×
14
from src.pyVertexModel.geometry import degreesOfFreedom
×
15
from src.pyVertexModel.geometry.geo import Geo
×
16
from src.pyVertexModel.mesh_remodelling.remodelling import Remodelling
×
17
from src.pyVertexModel.parameters.set import Set
×
18
from src.pyVertexModel.util.utils import save_state, save_backup_vars, load_backup_vars, copy_non_mutable_attributes
×
19

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

22

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

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

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

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

46
    plotter = pv.Plotter(off_screen=True)
×
47
    for _, cell in enumerate(v_model.geo.Cells):
×
UNCOV
48
        if cell.AliveStatus == 1 and (cell.ID in selected_cells or selected_cells is not []):
×
49
            # Load the VTK file as a pyvista mesh
UNCOV
50
            mesh = cell.create_pyvista_mesh()
×
51

52
            # Add the mesh to the plotter
UNCOV
53
            plotter.add_mesh(mesh, scalars='ID', lighting=True, cmap="prism", clim=v_model.colormap_lim,
×
54
                             show_edges=True, edge_opacity=0.5, edge_color='grey')
55
    # Set a fixed camera zoom level
UNCOV
56
    fixed_zoom_level = 1
×
UNCOV
57
    plotter.camera.zoom(fixed_zoom_level)
×
58

59
    # Add text to the plotter
60
    if v_model.set.ablation:
×
61
        timeAfterAblation = float(v_model.t) - float(v_model.set.TInitAblation)
×
62
        text_content = f"Ablation time: {timeAfterAblation:.2f}"
×
63
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
64
    else:
UNCOV
65
        text_content = f"Time: {v_model.t:.2f}"
×
66
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
67

68
    # Render the scene and capture a screenshot
69
    img = plotter.screenshot()
×
70
    # Save the image to a temporary file
71
    temp_file = os.path.join(temp_dir, f'vModel_perspective_{v_model.numStep}.png')
×
UNCOV
72
    imageio.imwrite(temp_file, img)
×
73

74
    # True 2D
UNCOV
75
    plotter.enable_parallel_projection()
×
UNCOV
76
    plotter.enable_image_style()
×
77

78
    # Set the camera to the top view
UNCOV
79
    plotter.view_xy()
×
80

UNCOV
81
    img = plotter.screenshot()
×
82
    temp_file = os.path.join(temp_dir, f'vModel_top_{v_model.numStep}.png')
×
83
    imageio.imwrite(temp_file, img)
×
84

85
    # Set the camera to the front view
86
    plotter.view_xz()
×
87

UNCOV
88
    img = plotter.screenshot()
×
UNCOV
89
    temp_file = os.path.join(temp_dir, f'vModel_front_{v_model.numStep}.png')
×
90
    imageio.imwrite(temp_file, img)
×
91

92
    # Set the camera to the bottom view
UNCOV
93
    plotter.view_xy(negative=True)
×
94

UNCOV
95
    img = plotter.screenshot()
×
UNCOV
96
    temp_file = os.path.join(temp_dir, f'vModel_bottom_{v_model.numStep}.png')
×
UNCOV
97
    imageio.imwrite(temp_file, img)
×
98

99
    # Close the plotter
UNCOV
100
    plotter.close()
×
101

UNCOV
102
def generate_tetrahedra_from_information(X, cell_edges, cell_height, cell_centroids, main_cells,
×
103
                                         neighbours_network, selected_planes, triangles_connectivity,
104
                                         vertices_of_cell_pos, geo):
105
    """
106
    Generate tetrahedra from the information of the cells.
107
    :param X:
108
    :param cell_edges:
109
    :param cell_height:
110
    :param cell_centroids:
111
    :param main_cells:
112
    :param neighbours_network:
113
    :param selected_planes:
114
    :param triangles_connectivity:
115
    :param vertices_of_cell_pos:
116
    :param geo:
117
    :return:
118
    """
119
    bottom_plane = 0
×
UNCOV
120
    top_plane = 1
×
121
    if bottom_plane == 0:
×
UNCOV
122
        z_coordinate = [-cell_height, cell_height]
×
123
    else:
124
        z_coordinate = [cell_height, -cell_height]
×
125
    Twg = []
×
126
    for idPlane, numPlane in enumerate(selected_planes):
×
127
        # Using the centroids and vertices of the cells of each 2D image as ghost nodes
128
        X, Xg_faceIds, Xg_ids, Xg_verticesIds = (
×
129
            add_faces_and_vertices_to_x(X,
130
                                        np.hstack((cell_centroids[numPlane][:, 1:3], np.tile(z_coordinate[idPlane],
131
                                                                                             (len(cell_centroids[
132
                                                                                                      numPlane][:,
133
                                                                                                  1:3]), 1)))),
134
                                        np.hstack((np.fliplr(vertices_of_cell_pos[numPlane]),
135
                                                   np.tile(z_coordinate[idPlane],
136
                                                           (len(vertices_of_cell_pos[numPlane]), 1))))))
137

138
        # Fill Geo info
UNCOV
139
        if idPlane == bottom_plane:
×
UNCOV
140
            geo.XgBottom = Xg_ids
×
UNCOV
141
        elif idPlane == top_plane:
×
UNCOV
142
            geo.XgTop = Xg_ids
×
143

144
        # Create tetrahedra
145
        Twg_numPlane = create_tetrahedra(triangles_connectivity[numPlane], neighbours_network[numPlane],
×
146
                                         cell_edges[numPlane], main_cells, Xg_faceIds, Xg_verticesIds)
147

148
        Twg.append(Twg_numPlane)
×
149
    Twg = np.vstack(Twg)
×
UNCOV
150
    return Twg, X
×
151

152

UNCOV
153
def add_faces_and_vertices_to_x(X, Xg_faceCentres2D, Xg_vertices2D):
×
154
    """
155
    Add faces and vertices to the X matrix.
156
    :param X:
157
    :param Xg_faceCentres2D:
158
    :param Xg_vertices2D:
159
    :return:
160
    """
UNCOV
161
    Xg_nodes = np.vstack((Xg_faceCentres2D, Xg_vertices2D))
×
UNCOV
162
    Xg_ids = np.arange(X.shape[0] + 1, X.shape[0] + Xg_nodes.shape[0] + 1)
×
163
    Xg_faceIds = Xg_ids[0:Xg_faceCentres2D.shape[0]]
×
164
    Xg_verticesIds = Xg_ids[Xg_faceCentres2D.shape[0]:]
×
165
    X = np.vstack((X, Xg_nodes))
×
166
    return X, Xg_faceIds, Xg_ids, Xg_verticesIds
×
167

168

169
def create_tetrahedra(triangles_connectivity, neighbours_network, edges_of_vertices, x_internal, x_face_ids,
×
170
                      x_vertices_ids):
171
    """
172
    Add connections between real nodes and ghost cells to create tetrahedra.
173

174
    :param triangles_connectivity: A 2D array where each row represents a triangle connectivity.
175
    :param neighbours_network: A 2D array where each row represents a pair of neighboring nodes.
176
    :param edges_of_vertices: A list of lists where each sublist represents the edges of a vertex.
177
    :param x_internal: A 1D array representing the internal nodes.
178
    :param x_face_ids: A 1D array representing the face ids.
179
    :param x_vertices_ids: A 1D array representing the vertices ids.
180
    :return: A 2D array representing the tetrahedra.
181
    """
182
    x_ids = np.concatenate([x_face_ids, x_vertices_ids])
×
183

184
    # Relationships: 1 ghost node, three cell nodes
UNCOV
185
    twg = np.hstack([triangles_connectivity, x_vertices_ids[:, None]])
×
186

187
    # Relationships: 1 cell node and 3 ghost nodes
UNCOV
188
    new_additions = []
×
UNCOV
189
    for id_cell, num_cell in enumerate(x_internal):
×
190
        face_id = x_face_ids[id_cell]
×
191
        vertices_to_connect = edges_of_vertices[id_cell]
×
UNCOV
192
        new_additions.extend(np.hstack([np.repeat(np.array([[num_cell, face_id]]), len(vertices_to_connect), axis=0),
×
193
                                        x_vertices_ids[vertices_to_connect]]))
194
    twg = np.vstack([twg, new_additions])
×
195

196
    # Relationships: 2 ghost nodes, two cell nodes
197
    twg_sorted = np.sort(twg[np.any(np.isin(twg, x_ids), axis=1)], axis=1)
×
198
    internal_neighbour_network = [neighbour for neighbour in neighbours_network if
×
199
                                  np.any(np.isin(neighbour, x_internal))]
200
    internal_neighbour_network = np.unique(np.sort(internal_neighbour_network, axis=1), axis=0)
×
201

202
    new_additions = []
×
203
    for num_pair in range(internal_neighbour_network.shape[0]):
×
204
        found = np.isin(twg_sorted, internal_neighbour_network[num_pair])
×
UNCOV
205
        new_connections = np.unique(twg_sorted[np.sum(found, axis=1) == 2, 3])
×
206
        if len(new_connections) > 1:
×
UNCOV
207
            new_connections_pairs = np.array(list(combinations(new_connections, 2)))
×
UNCOV
208
            new_additions.extend([np.hstack([internal_neighbour_network[num_pair], new_connections_pair])
×
209
                                  for new_connections_pair in new_connections_pairs])
210
        else:
UNCOV
211
            raise ValueError('Error while creating the connections and initial topology')
×
UNCOV
212
    twg = np.vstack([twg, new_additions])
×
213

UNCOV
214
    return twg
×
215

216

UNCOV
217
def calculate_cell_height_on_model(img2DLabelled, main_cells, c_set):
×
218
    """
219
    Calculate the cell height on the model regarding the diameter of the cells.
220
    :param img2DLabelled:
221
    :param main_cells:
222
    :return:
223
    """
224
    properties = regionprops(img2DLabelled)
×
225
    # Extract major axis lengths
UNCOV
226
    avg_diameter = np.mean([prop.major_axis_length for prop in properties if prop.label in main_cells])
×
227
    cell_height = avg_diameter * c_set.CellHeight
×
UNCOV
228
    return cell_height
×
229

230

UNCOV
231
class VertexModel:
×
232
    """
233
    The main class for the vertex model simulation. It contains the methods for initializing the model,
234
    iterating over time, applying Brownian motion, and checking the integrity of the model.
235
    """
236

237
    def __init__(self, c_set=None, create_output_folder=True, update_derived_parameters=True):
×
238
        """
239
        Vertex Model class.
240
        :param c_set:
241
        """
242
        self.colormap_lim = None
×
243
        self.OutputFolder = None
×
244
        self.numStep = None
×
UNCOV
245
        self.backupVars = None
×
246
        self.geo_n = None
×
247
        self.geo_0 = None
×
UNCOV
248
        self.tr = None
×
UNCOV
249
        self.t = None
×
250
        self.X = None
×
251
        self.didNotConverge = False
×
UNCOV
252
        self.geo = Geo()
×
253

254
        # Set definition
UNCOV
255
        if c_set is not None:
×
256
            self.set = c_set
×
257
        else:
258
            # TODO Create a menu to select the set
259
            self.set = Set()
×
260
            # self.set.cyst()
UNCOV
261
            self.set.wing_disc()
×
262
            if self.set.ablation:
×
UNCOV
263
                self.set.wound_default()
×
264

UNCOV
265
            if update_derived_parameters:
×
UNCOV
266
                self.set.update_derived_parameters()
×
267

268
        # Redirect output
269
        if self.set.OutputFolder is not None and create_output_folder:
×
270
            self.set.redirect_output()
×
271

272
        # Degrees of freedom definition
UNCOV
273
        self.Dofs = degreesOfFreedom.DegreesOfFreedom()
×
274

275
        self.relaxingNu = False
×
276
        self.EnergiesPerTimeStep = []
×
277
        self.t = 0
×
278
        self.tr = 0
×
279
        self.numStep = 1
×
280

281
    @abstractmethod
×
UNCOV
282
    def initialize(self):
×
283
        pass
×
284

UNCOV
285
    def brownian_motion(self, scale):
×
286
        """
287
        Applies Brownian motion to the vertices of cells in the Geo structure.
288
        Displacements are generated with a normal distribution in each dimension.
289
        :param scale:
290
        :return:
291
        """
292

293
        # Concatenate and sort all tetrahedron vertices
294
        all_tets = np.sort(np.vstack([cell.T for cell in self.geo.Cells]), axis=1)
×
295
        all_tets_unique = np.unique(all_tets, axis=0)
×
296

297
        # Generate random displacements with a normal distribution for each dimension
UNCOV
298
        displacements = scale * (self.geo.Cells[0].X - self.geo.Cells[1].X) * np.random.randn(all_tets_unique.shape[0],
×
299
                                                                                              3)
300

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

UNCOV
306
    def iterate_over_time(self):
×
307
        """
308
        Iterate the model over time. This includes updating the degrees of freedom, applying boundary conditions,
309
        updating measures, and checking for convergence.
310
        :return:
311
        """
UNCOV
312
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
313
        if not os.path.exists(temp_dir):
×
UNCOV
314
            os.makedirs(temp_dir)
×
315

UNCOV
316
        if self.set.Substrate == 1:
×
UNCOV
317
            self.Dofs.GetDOFsSubstrate(self.geo, self.set)
×
318
        else:
UNCOV
319
            self.Dofs.get_dofs(self.geo, self.set)
×
320

321
        self.geo.remodelling = False
×
UNCOV
322
        if self.geo_0 is None:
×
323
            self.geo_0 = self.geo.copy(update_measurements=False)
×
324

325
        if self.geo_n is None:
×
UNCOV
326
            self.geo_n = self.geo.copy(update_measurements=False)
×
327

328
        # Count the number of faces in average has a cell per domain
UNCOV
329
        self.geo.update_barrier_tri0_based_on_number_of_faces()
×
330
        self.backupVars = save_backup_vars(self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs)
×
331

332
        print("File: ", self.set.OutputFolder)
×
333
        self.save_v_model_state()
×
334

UNCOV
335
        while self.t <= self.set.tend and not self.didNotConverge:
×
336
            gr = self.single_iteration()
×
337

338
            if np.isnan(gr):
×
339
                break
×
340

341
        return self.didNotConverge
×
342

343
    def single_iteration(self, post_operations=True):
×
344
        """
345
        Perform a single iteration of the model.
346
        :return:
347
        """
UNCOV
348
        self.set.currentT = self.t
×
UNCOV
349
        logger.info("Time: " + str(self.t))
×
350
        if not self.relaxingNu:
×
UNCOV
351
            self.set.i_incr = self.numStep
×
352

353
            # Ablate cells if needed
UNCOV
354
            if self.set.ablation:
×
355
                if self.set.ablation and self.set.TInitAblation <= self.t and self.geo.cellsToAblate is not None:
×
356
                    self.save_v_model_state(file_name='before_ablation')
×
357
                self.geo.ablate_cells(self.set, self.t)
×
UNCOV
358
                self.geo_n = self.geo.copy()
×
359
                # Update the degrees of freedom
360
                self.Dofs.get_dofs(self.geo, self.set)
×
361

UNCOV
362
            self.Dofs.ApplyBoundaryCondition(self.t, self.geo, self.set)
×
363
            # IMPORTANT: Here it updates: Areas, Volumes, etc... Should be
364
            # up-to-date
UNCOV
365
            self.geo.update_measures()
×
366
        if self.set.implicit_method is True:
×
367
            g, K, _, energies = newtonRaphson.KgGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
368
                                                       self.set.implicit_method)
369
        else:
UNCOV
370
            K = 0
×
UNCOV
371
            g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
372
                                                self.set.implicit_method)
373
        for key, energy in energies.items():
×
UNCOV
374
            logger.info(f"{key}: {energy}")
×
UNCOV
375
        self.geo, g, __, __, self.set, gr, dyr, dy = newtonRaphson.newton_raphson(self.geo_0, self.geo_n, self.geo,
×
376
                                                                                  self.Dofs, self.set, K, g,
377
                                                                                  self.numStep, self.t,
378
                                                                                  self.set.implicit_method)
UNCOV
379
        if not np.isnan(gr) and post_operations:
×
UNCOV
380
            self.post_newton_raphson(dy, dyr, g, gr)
×
UNCOV
381
        return gr
×
382

383
    def post_newton_raphson(self, dy, dyr, g, gr):
×
384
        """
385
        Post Newton Raphson operations.
386
        :param dy:
387
        :param dyr:
388
        :param g:
389
        :param gr:
390
        :return:
391
        """
UNCOV
392
        if (gr < self.set.tol and dyr < self.set.tol and np.all(~np.isnan(g[self.Dofs.Free])) and
×
393
                np.all(~np.isnan(dy[self.Dofs.Free]))):
394
            self.iteration_converged()
×
395
            # if self.set.implicit_method is False:
396
            #     self.set.tol = gr
397
            #     if self.set.tol < self.set.tol0:
398
            #         self.set.tol = self.set.tol0
399
        else:
UNCOV
400
            self.iteration_did_not_converged()
×
401

UNCOV
402
        self.Dofs.get_dofs(self.geo, self.set)
×
403

404
    def iteration_did_not_converged(self):
×
405
        """
406
        If the iteration did not converge, the algorithm will try to relax the value of nu and dt.
407
        :return:
408
        """
UNCOV
409
        self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs = load_backup_vars(self.backupVars)
×
410
        self.relaxingNu = False
×
UNCOV
411
        if self.set.iter == self.set.MaxIter0 and self.set.implicit_method:
×
UNCOV
412
            self.set.MaxIter = self.set.MaxIter0 * 3
×
UNCOV
413
            self.set.nu = 10 * self.set.nu0
×
414
        else:
UNCOV
415
            if (self.set.iter >= self.set.MaxIter and
×
416
                    (self.set.dt / self.set.dt0) > 1e-6):
417
                self.set.MaxIter = self.set.MaxIter0
×
418
                self.set.nu = self.set.nu0
×
419
                self.set.dt = self.set.dt / 2
×
420
                self.t = self.set.last_t_converged + self.set.dt
×
421
            else:
422
                self.didNotConverge = True
×
423

UNCOV
424
    def iteration_converged(self):
×
425
        """
426
        If the iteration converged, the algorithm will update the values of the variables and proceed to the next step.
427
        :return:
428
        """
UNCOV
429
        if self.set.nu / self.set.nu0 == 1:
×
430
            # STEP has converged
UNCOV
431
            logger.info(f"STEP {str(self.set.i_incr)} has converged ...")
×
432

433
            # Remodelling
434
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
435
                if self.set.Remodelling:
×
436
                    save_state(self,
×
437
                               os.path.join(self.set.OutputFolder,
438
                                            'data_step_before_remodelling_' + str(self.numStep) + '.pkl'))
439
                    remodel_obj = Remodelling(self.geo, self.geo_n, self.geo_0, self.set, self.Dofs)
×
440
                    self.geo, self.geo_n = remodel_obj.remodel_mesh(self.numStep)
×
441
                    # Update tolerance if remodelling was performed to the current one
442
                    if self.set.implicit_method is False:
×
443
                        g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
444
                                                            self.set.implicit_method)
445
                        self.Dofs.get_dofs(self.geo, self.set)
×
446
                        gr = np.linalg.norm(g[self.Dofs.Free])
×
447

448
            # Build X From Y
UNCOV
449
            self.geo.build_x_from_y(self.geo_n)
×
450

451
            # Update last time converged
UNCOV
452
            self.set.last_t_converged = self.t
×
453

454
            # Test Geo
455
            # TODO: CHECK
456
            # self.check_integrity()
457

458
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
459
                self.save_v_model_state()
×
460

461
                # Reset noise to be comparable between simulations
UNCOV
462
                self.reset_noisy_parameters()
×
UNCOV
463
                self.tr = self.t
×
464

465
                # Brownian Motion
UNCOV
466
                if self.set.brownian_motion is True:
×
467
                    self.brownian_motion(self.set.brownian_motion_scale)
×
468

469
            self.t = self.t + self.set.dt
×
470
            self.set.dt = np.min([self.set.dt + self.set.dt * 0.5, self.set.dt0])
×
471
            self.set.MaxIter = self.set.MaxIter0
×
472
            self.numStep = self.numStep + 1
×
UNCOV
473
            self.backupVars = {
×
474
                'Geo_b': self.geo.copy(),
475
                'Geo_n_b': self.geo_n.copy(),
476
                'Geo_0_b': self.geo_0.copy(),
477
                'tr_b': self.tr,
478
                'Dofs': self.Dofs.copy()
479
            }
480
            self.geo_n = self.geo.copy()
×
481
            self.relaxingNu = False
×
482
        else:
UNCOV
483
            self.set.nu = np.max([self.set.nu / 2, self.set.nu0])
×
UNCOV
484
            self.relaxingNu = True
×
485

UNCOV
486
    def save_v_model_state(self, file_name=None):
×
487
        """
488
        Save the state of the vertex model.
489
        :param file_name:
490
        :return:
491
        """
492
        # Create VTK files for the current state
493
        self.geo.create_vtk_cell(self.set, self.numStep, 'Edges')
×
494
        self.geo.create_vtk_cell(self.set, self.numStep, 'Cells')
×
UNCOV
495
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
496
        screenshot(self, temp_dir)
×
497
        # Save Data of the current step
498
        if file_name is None:
×
UNCOV
499
            save_state(self, os.path.join(self.set.OutputFolder, 'data_step_' + str(self.numStep) + '.pkl'))
×
500
        else:
UNCOV
501
            save_state(self, os.path.join(self.set.OutputFolder, file_name + '.pkl'))
×
502

UNCOV
503
    def reset_noisy_parameters(self):
×
504
        """
505
        Reset noisy parameters.
506
        :return:
507
        """
508
        for num_cell in range(len(self.geo.Cells)):
×
509
            c_cell = self.geo.Cells[num_cell]
×
510
            self.geo.Cells[num_cell].contractlity_noise = None
×
UNCOV
511
            self.geo.Cells[num_cell].lambda_s1_noise = None
×
UNCOV
512
            self.geo.Cells[num_cell].lambda_s2_noise = None
×
513
            self.geo.Cells[num_cell].lambda_s3_noise = None
×
514
            self.geo.Cells[num_cell].lambda_v_noise = None
×
515
            for n_face in range(len(c_cell.Faces)):
×
516
                face = c_cell.Faces[n_face]
×
517
                for n_tri in range(len(face.Tris)):
×
UNCOV
518
                    tri = face.Tris[n_tri]
×
UNCOV
519
                    tri.ContractilityValue = None
×
520
                    tri.lambda_r_noise = None
×
521
                    tri.lambda_b_noise = None
×
522
                    tri.k_substrate_noise = None
×
523
                    # tri.edge_length_time.append([self.t, tri.edge_length])
524
                    self.geo.Cells[num_cell].Faces[n_face].Tris[n_tri] = tri
×
525

UNCOV
526
    def check_integrity(self):
×
527
        """
528
        Performs tests on the properties of cells, faces, and triangles (tris) within the Geo structure.
529
        Ensures that certain geometrical properties are above minimal threshold values.
530
        """
531

532
        # Define minimum error thresholds for edge length, area, and volume
UNCOV
533
        min_error_edge = 1e-5
×
534
        min_error_area = min_error_edge ** 2
×
UNCOV
535
        min_error_volume = min_error_edge ** 3
×
536

537
        # Test Cells properties:
538
        # Conditions checked:
539
        # - Volume > minimum error volume
540
        # - Initial Volume > minimum error volume
541
        # - Area > minimum error area
542
        # - Initial Area > minimum error area
UNCOV
543
        for c_cell in self.geo.Cells:
×
UNCOV
544
            if c_cell.AliveStatus:
×
UNCOV
545
                assert c_cell.Vol > min_error_volume, "Cell volume is too low"
×
UNCOV
546
                assert c_cell.Vol0 > min_error_volume, "Cell initial volume is too low"
×
UNCOV
547
                assert c_cell.Area > min_error_area, "Cell area is too low"
×
UNCOV
548
                assert c_cell.Area0 > min_error_area, "Cell initial area is too low"
×
549

550
        # Test Faces properties:
551
        # Conditions checked:
552
        # - Area > minimum error area
553
        # - Initial Area > minimum error area
UNCOV
554
        for c_cell in self.geo.Cells:
×
555
            if c_cell.AliveStatus:
×
UNCOV
556
                for face in c_cell.Faces:
×
557
                    assert face.Area > min_error_area, "Face area is too low"
×
UNCOV
558
                    assert face.Area0 > min_error_area, "Face initial area is too low"
×
559

560
        # Test Tris properties:
561
        # Conditions checked:
562
        # - Edge length > minimum error edge length
563
        # - Any Lengths to Centre > minimum error edge length
564
        # - Area > minimum error area
565
        for c_cell in self.geo.Cells:
×
566
            if c_cell.AliveStatus:
×
567
                for face in c_cell.Faces:
×
UNCOV
568
                    for tris in face.Tris:
×
569
                        assert tris.EdgeLength > min_error_edge, "Triangle edge length is too low"
×
UNCOV
570
                        assert any(length > min_error_edge for length in
×
571
                                   tris.LengthsToCentre), "Triangle lengths to centre are too low"
572
                        assert tris.Area > min_error_area, "Triangle area is too low"
×
573

UNCOV
574
    def analyse_vertex_model(self):
×
575
        """
576
        Analyse the vertex model.
577
        :return:
578
        """
579
        # Initialize average cell properties
580
        cell_features = []
×
581
        debris_features = []
×
582

583
        wound_centre, debris_cells = self.geo.compute_wound_centre()
×
UNCOV
584
        list_of_cell_distances = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=None)
×
UNCOV
585
        list_of_cell_distances_top = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=0)
×
586
        list_of_cell_distances_bottom = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=2)
×
587

588
        # Analyse the alive cells
589
        for cell_id, cell in enumerate(self.geo.Cells):
×
590
            if cell.AliveStatus:
×
UNCOV
591
                cell_features.append(cell.compute_features(wound_centre))
×
UNCOV
592
            elif cell.AliveStatus is not None:
×
593
                debris_features.append(cell.compute_features())
×
594

595
        # Calculate average of cell features
596
        all_cell_features = pd.DataFrame(cell_features)
×
UNCOV
597
        all_cell_features["cell_distance_to_wound"] = list_of_cell_distances
×
598
        all_cell_features["cell_distance_to_wound_top"] = list_of_cell_distances_top
×
599
        all_cell_features["cell_distance_to_wound_bottom"] = list_of_cell_distances_bottom
×
UNCOV
600
        all_cell_features["time"] = self.t
×
UNCOV
601
        avg_cell_features = all_cell_features.mean()
×
602

603
        # Compute wound features
604
        try:
×
605
            wound_features = self.compute_wound_features()
×
UNCOV
606
            avg_cell_features = pd.concat([avg_cell_features, pd.Series(wound_features)])
×
UNCOV
607
        except Exception as e:
×
608
            pass
×
609

UNCOV
610
        return all_cell_features, avg_cell_features
×
611

612
    def compute_wound_features(self):
×
613
        """
614
        Compute wound features.
615
        :return:
616
        """
UNCOV
617
        wound_features = {
×
618
            'num_cells_wound_edge': len(self.geo.compute_cells_wound_edge()),
619
            'num_cells_wound_edge_top': len(self.geo.compute_cells_wound_edge(location_filter="Top")),
620
            'num_cells_wound_edge_bottom': len(self.geo.compute_cells_wound_edge(location_filter="Bottom")),
621
            'wound_area_top': self.geo.compute_wound_area(location_filter="Top"),
622
            'wound_area_bottom': self.geo.compute_wound_area(location_filter="Bottom"),
623
            'wound_volume': self.geo.compute_wound_volume(),
624
            'wound_height': self.geo.compute_wound_height(),
625
            'wound_aspect_ratio_top': self.geo.compute_wound_aspect_ratio(location_filter="Top"),
626
            'wound_aspect_ratio_bottom': self.geo.compute_wound_aspect_ratio(location_filter="Bottom"),
627
            'wound_perimeter_top': self.geo.compute_wound_perimeter(location_filter="Top"),
628
            'wound_perimeter_bottom': self.geo.compute_wound_perimeter(location_filter="Bottom")
629
        }
630

UNCOV
631
        return wound_features
×
632

633
    def copy(self):
×
634
        """
635
        Copy the VertexModel object.
636
        :return:
637
        """
UNCOV
638
        new_v_model = VertexModel()
×
UNCOV
639
        copy_non_mutable_attributes(self, '', new_v_model)
×
640

641
        return new_v_model
×
642

643
    def calculate_error(self, K, initial_recoil, error_type=None):
×
644
        """
645
        Calculate the error of the model.
646
        :return:
647
        """
648
        # The error consist on:
649
        # - There shouldn't be any cells with very small area in the top or bottom domain.
650
        # - It should get until the end of the simulation (tend).
651
        # - When ablating, it should get to: 165 percentage of area at 35.8 minutes.
UNCOV
652
        error = 0
×
653

654
        # Check if the simulation reached the end
UNCOV
655
        if self.t < self.set.tend:
×
UNCOV
656
            error += (self.t - self.set.tend) ** 2
×
657

658
        # # Check how many cells have a very small area
UNCOV
659
        if error_type == 'None' or 'SmallArea' in error_type:
×
UNCOV
660
            std_area_top = np.std([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
661
            std_area_bottom = np.std([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
662
            mean_area_top = np.mean([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
663
            mean_area_bottom = np.mean([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
664
            zscore_area_top = std_area_top / mean_area_top
×
665
            zscore_area_bottom = std_area_bottom / mean_area_bottom
×
666
            error += zscore_area_top ** 2
×
667
            error += zscore_area_bottom ** 2
×
668

669
        # Check how similar the recoil from in vivo is to the initial recoil and K value
UNCOV
670
        correct_K = 0.126
×
UNCOV
671
        correct_initial_recoil = 0.213
×
672
        if error_type == 'None' or 'K' in error_type:
×
673
            try:
×
674
                error += np.abs(K[0] - correct_K) * 100
×
675
            except IndexError:
×
676
                error += np.abs(K - correct_K) * 100
×
677

678
        if error_type == 'None' or 'InitialRecoil' in error_type:
×
UNCOV
679
            try:
×
680
                error += np.abs(initial_recoil[0] - correct_initial_recoil) * 100
×
681
            except IndexError:
×
682
                error += np.abs(initial_recoil - correct_initial_recoil) * 100
×
683

684
        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