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

Pablo1990 / pyVertexModel / 11776819502

11 Nov 2024 10:34AM UTC coverage: 0.797% (+0.005%) from 0.792%
11776819502

push

github

Pablo1990
intercalations seem to work?????

0 of 995 branches covered (0.0%)

Branch coverage included in aggregate %.

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

355 existing lines in 6 files now uncovered.

51 of 5408 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
×
UNCOV
19
from src.pyVertexModel.util.utils import save_state, save_backup_vars, load_backup_vars, copy_non_mutable_attributes
×
20

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

23

UNCOV
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:
×
UNCOV
35
            return
×
36

UNCOV
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:
×
UNCOV
41
        v_model.colormap_lim = [0, total_real_cells]
×
42

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

153

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

169

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

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

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

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

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

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

UNCOV
215
    return twg
×
216

217

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

231

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

329
        self.backupVars = save_backup_vars(self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs)
×
330

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

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

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

UNCOV
340
        return self.didNotConverge
×
341

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

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

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

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

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

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

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

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

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

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

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

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

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

462
                # Reset noise to be comparable between simulations
UNCOV
463
                self.reset_noisy_parameters()
×
464
                # Count the number of faces in average has a cell per domain
465
                self.geo.update_barrier_tri0_based_on_number_of_faces()
×
UNCOV
466
                self.tr = self.t
×
467

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

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

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

UNCOV
506
    def reset_noisy_parameters(self):
×
507
        """
508
        Reset noisy parameters.
509
        :return:
510
        """
511
        for cell in self.geo.Cells:
×
512
            cell.lambda_s1_perc = add_noise_to_parameter(1, self.set.noise_random)
×
513
            cell.lambda_s2_perc = add_noise_to_parameter(1, self.set.noise_random)
×
514
            cell.lambda_s3_perc = add_noise_to_parameter(1, self.set.noise_random)
×
515
            cell.lambda_v_perc = add_noise_to_parameter(1, self.set.noise_random)
×
516
            cell.lambda_r_perc = add_noise_to_parameter(1, self.set.noise_random)
×
517
            cell.c_line_tension_perc = add_noise_to_parameter(1, self.set.noise_random)
×
518
            cell.k_substrate_perc = add_noise_to_parameter(1, self.set.noise_random)
×
519
            cell.lambda_b_perc = add_noise_to_parameter(1, self.set.noise_random)
×
520

521
    def check_integrity(self):
×
522
        """
523
        Performs tests on the properties of cells, faces, and triangles (tris) within the Geo structure.
524
        Ensures that certain geometrical properties are above minimal threshold values.
525
        """
526

527
        # Define minimum error thresholds for edge length, area, and volume
528
        min_error_edge = 1e-5
×
UNCOV
529
        min_error_area = min_error_edge ** 2
×
UNCOV
530
        min_error_volume = min_error_edge ** 3
×
531

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

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

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

569
    def analyse_vertex_model(self):
×
570
        """
571
        Analyse the vertex model.
572
        :return:
573
        """
574
        # Initialize average cell properties
UNCOV
575
        cell_features = []
×
576
        debris_features = []
×
577

UNCOV
578
        wound_centre, debris_cells = self.geo.compute_wound_centre()
×
UNCOV
579
        list_of_cell_distances = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=None)
×
UNCOV
580
        list_of_cell_distances_top = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=0)
×
UNCOV
581
        list_of_cell_distances_bottom = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=2)
×
582

583
        # Analyse the alive cells
UNCOV
584
        for cell_id, cell in enumerate(self.geo.Cells):
×
585
            if cell.AliveStatus:
×
586
                cell_features.append(cell.compute_features(wound_centre))
×
587
            elif cell.AliveStatus is not None:
×
588
                debris_features.append(cell.compute_features())
×
589

590
        # Calculate average of cell features
591
        all_cell_features = pd.DataFrame(cell_features)
×
592
        all_cell_features["cell_distance_to_wound"] = list_of_cell_distances
×
593
        all_cell_features["cell_distance_to_wound_top"] = list_of_cell_distances_top
×
594
        all_cell_features["cell_distance_to_wound_bottom"] = list_of_cell_distances_bottom
×
595
        all_cell_features["time"] = self.t
×
UNCOV
596
        avg_cell_features = all_cell_features.mean()
×
597

598
        # Compute wound features
599
        try:
×
600
            wound_features = self.compute_wound_features()
×
601
            avg_cell_features = pd.concat([avg_cell_features, pd.Series(wound_features)])
×
602
        except Exception as e:
×
603
            pass
×
604

UNCOV
605
        return all_cell_features, avg_cell_features
×
606

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

UNCOV
626
        return wound_features
×
627

UNCOV
628
    def copy(self):
×
629
        """
630
        Copy the VertexModel object.
631
        :return:
632
        """
633
        new_v_model = VertexModel()
×
UNCOV
634
        copy_non_mutable_attributes(self, '', new_v_model)
×
635

UNCOV
636
        return new_v_model
×
637

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

649
        # Check if the simulation reached the end
UNCOV
650
        if self.t < self.set.tend:
×
UNCOV
651
            error += (self.t - self.set.tend) ** 2
×
652

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

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

673
        if error_type == 'None' or 'InitialRecoil' in error_type:
×
674
            try:
×
675
                error += np.abs(initial_recoil[0] - correct_initial_recoil) * 100
×
676
            except IndexError:
×
677
                error += np.abs(initial_recoil - correct_initial_recoil) * 100
×
678

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