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

Pablo1990 / pyVertexModel / 11570987312

28 Oct 2024 10:05AM UTC coverage: 0.794% (-0.002%) from 0.796%
11570987312

push

github

Pablo1990
for now onlyy top and edge remodelling

0 of 1003 branches covered (0.0%)

Branch coverage included in aggregate %.

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

283 existing lines in 3 files now uncovered.

51 of 5424 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
32
    if hasattr(v_model.set, 'export_images'):
×
33
        if v_model.set.export_images is False:
×
34
            return
×
35

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
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:
×
44
        selected_cells = []
×
45

46
    plotter = pv.Plotter(off_screen=True)
×
47
    for _, cell in enumerate(v_model.geo.Cells):
×
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
50
            mesh = cell.create_pyvista_mesh()
×
51

52
            # Add the mesh to the plotter
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
56
    fixed_zoom_level = 1
×
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:
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')
×
72
    imageio.imwrite(temp_file, img)
×
73

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

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

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

88
    img = plotter.screenshot()
×
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
93
    plotter.view_xy(negative=True)
×
94

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

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

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
×
120
    top_plane = 1
×
121
    if bottom_plane == 0:
×
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
139
        if idPlane == bottom_plane:
×
140
            geo.XgBottom = Xg_ids
×
141
        elif idPlane == top_plane:
×
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)
×
150
    return Twg, X
×
151

152

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
    """
161
    Xg_nodes = np.vstack((Xg_faceCentres2D, Xg_vertices2D))
×
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
185
    twg = np.hstack([triangles_connectivity, x_vertices_ids[:, None]])
×
186

187
    # Relationships: 1 cell node and 3 ghost nodes
188
    new_additions = []
×
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]
×
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])
×
205
        new_connections = np.unique(twg_sorted[np.sum(found, axis=1) == 2, 3])
×
206
        if len(new_connections) > 1:
×
207
            new_connections_pairs = np.array(list(combinations(new_connections, 2)))
×
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:
211
            raise ValueError('Error while creating the connections and initial topology')
×
212
    twg = np.vstack([twg, new_additions])
×
213

214
    return twg
×
215

216

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
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
×
228
    return cell_height
×
229

230

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
×
245
        self.backupVars = None
×
246
        self.geo_n = None
×
247
        self.geo_0 = None
×
248
        self.tr = None
×
249
        self.t = None
×
250
        self.X = None
×
251
        self.didNotConverge = False
×
252
        self.geo = Geo()
×
253

254
        # Set definition
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()
261
            self.set.wing_disc()
×
262
            if self.set.ablation:
×
263
                self.set.wound_default()
×
264

265
            if update_derived_parameters:
×
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
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
×
282
    def initialize(self):
×
283
        pass
×
284

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
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]:
×
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

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
        """
312
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
313
        if not os.path.exists(temp_dir):
×
314
            os.makedirs(temp_dir)
×
315

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

321
        self.geo.remodelling = False
×
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:
×
326
            self.geo_n = self.geo.copy(update_measurements=False)
×
327

328
        # Count the number of faces in average has a cell per domain
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

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
        """
348
        self.set.currentT = self.t
×
349
        logger.info("Time: " + str(self.t))
×
350
        if not self.relaxingNu:
×
351
            self.set.i_incr = self.numStep
×
352

353
            # Ablate cells if needed
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)
×
358
                self.geo_n = self.geo.copy()
×
359
                # Update the degrees of freedom
360
                self.Dofs.get_dofs(self.geo, self.set)
×
361

362
            self.Dofs.ApplyBoundaryCondition(self.t, self.geo, self.set)
×
363
            # IMPORTANT: Here it updates: Areas, Volumes, etc... Should be
364
            # up-to-date
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:
370
            K = 0
×
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():
×
374
            logger.info(f"{key}: {energy}")
×
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)
379
        if not np.isnan(gr) and post_operations:
×
380
            self.post_newton_raphson(dy, dyr, g, gr)
×
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
        """
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:
400
            self.iteration_did_not_converged()
×
401

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
        """
409
        self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs = load_backup_vars(self.backupVars)
×
410
        self.relaxingNu = False
×
411
        if self.set.iter == self.set.MaxIter0 and self.set.implicit_method:
×
412
            self.set.MaxIter = self.set.MaxIter0 * 3
×
413
            self.set.nu = 10 * self.set.nu0
×
414
        else:
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

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
        """
429
        if self.set.nu / self.set.nu0 == 1:
×
430
            # STEP has converged
431
            logger.info(f"STEP {str(self.set.i_incr)} has converged ...")
×
432

433
            # Build X From Y
434
            self.geo.build_x_from_y(self.geo_n)
×
435

436
            # Remodelling
UNCOV
437
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
UNCOV
438
                if self.set.Remodelling:
×
439
                    save_state(self,
×
440
                               os.path.join(self.set.OutputFolder,
441
                                            'data_step_before_remodelling_' + str(self.numStep) + '.pkl'))
442

443
                    # Remodelling
UNCOV
444
                    remodel_obj = Remodelling(self.geo, self.geo_n, self.geo_0, self.set, self.Dofs)
×
445
                    self.geo, self.geo_n = remodel_obj.remodel_mesh(self.numStep)
×
446
                    # Update tolerance if remodelling was performed to the current one
UNCOV
447
                    if self.set.implicit_method is False:
×
UNCOV
448
                        g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
449
                                                            self.set.implicit_method)
UNCOV
450
                        self.Dofs.get_dofs(self.geo, self.set)
×
UNCOV
451
                        gr = np.linalg.norm(g[self.Dofs.Free])
×
452

453
            # Update last time converged
UNCOV
454
            self.set.last_t_converged = self.t
×
455

456
            # Test Geo
457
            # TODO: CHECK
458
            # self.check_integrity()
459

UNCOV
460
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
UNCOV
461
                self.save_v_model_state()
×
462

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

612
        return all_cell_features, avg_cell_features
×
613

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

633
        return wound_features
×
634

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

643
        return new_v_model
×
644

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

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

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

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

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

UNCOV
686
        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