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

Pablo1990 / pyVertexModel / 11836362134

14 Nov 2024 11:19AM UTC coverage: 0.789% (-0.002%) from 0.791%
11836362134

push

github

Pablo1990
Displaying edges and different perspectives

Also, better colours

0 of 1010 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 19 new or added lines in 2 files covered. (0.0%)

164 existing lines in 2 files now uncovered.

51 of 5454 relevant lines covered (0.94%)

0.01 hits per line

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

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

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

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

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

23

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

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

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

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

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

53
            # Add the mesh to the plotter
NEW
54
            plotter.add_mesh(mesh, name=f'cell_{cell.ID}', scalars='ID', lighting=True, cmap="tab20b", clim=v_model.colormap_lim,
×
55
                             show_edges=False, edge_opacity=0.5, edge_color='grey')
56

57
    for _, cell in enumerate(v_model.geo.Cells):
×
58
        if cell.AliveStatus == 1 and (cell.ID in selected_cells or selected_cells is not []):
×
UNCOV
59
            edge_mesh = cell.create_pyvista_edges()
×
NEW
60
            plotter.add_mesh(edge_mesh, name=f'edge_{cell.ID}', color='black', line_width=1)
×
61

62
    # Set a fixed camera zoom level
UNCOV
63
    fixed_zoom_level = 1
×
64
    plotter.camera.zoom(fixed_zoom_level)
×
65

66
    # Add text to the plotter
UNCOV
67
    if v_model.set.ablation:
×
68
        timeAfterAblation = float(v_model.t) - float(v_model.set.TInitAblation)
×
UNCOV
69
        text_content = f"Ablation time: {timeAfterAblation:.2f}"
×
70
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
71
    else:
UNCOV
72
        text_content = f"Time: {v_model.t:.2f}"
×
UNCOV
73
        plotter.add_text(text_content, position='upper_right', font_size=12, color='black')
×
74

75

76

77
    # Render the scene and capture a screenshot
UNCOV
78
    img = plotter.screenshot(transparent_background=True, scale=3)
×
79
    # Save the image to a temporary file
80
    temp_file = os.path.join(temp_dir, f'vModel_perspective_{v_model.numStep}.png')
×
UNCOV
81
    imageio.imwrite(temp_file, img)
×
82

83
    # True 2D
84
    plotter.enable_parallel_projection()
×
UNCOV
85
    plotter.enable_image_style()
×
86

87
    # Set the camera to the top view
UNCOV
88
    plotter.view_xy()
×
89

90
    img = plotter.screenshot(transparent_background=True, scale=3)
×
91
    temp_file = os.path.join(temp_dir, f'vModel_top_{v_model.numStep}.png')
×
UNCOV
92
    imageio.imwrite(temp_file, img)
×
93

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

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

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

UNCOV
112
    img = plotter.screenshot(transparent_background=True, scale=3)
×
NEW
113
    temp_file = os.path.join(temp_dir, f'vModel_front_{v_model.numStep}.png')
×
UNCOV
114
    imageio.imwrite(temp_file, img)
×
115

116
    # Close the plotter
UNCOV
117
    plotter.close()
×
118

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

155
        # Fill Geo info
UNCOV
156
        if idPlane == bottom_plane:
×
157
            geo.XgBottom = Xg_ids
×
158
        elif idPlane == top_plane:
×
159
            geo.XgTop = Xg_ids
×
160

161
        # Create tetrahedra
162
        Twg_numPlane = create_tetrahedra(triangles_connectivity[numPlane], neighbours_network[numPlane],
×
163
                                         cell_edges[numPlane], main_cells, Xg_faceIds, Xg_verticesIds)
164

UNCOV
165
        Twg.append(Twg_numPlane)
×
UNCOV
166
    Twg = np.vstack(Twg)
×
UNCOV
167
    return Twg, X
×
168

169

170
def add_faces_and_vertices_to_x(X, Xg_faceCentres2D, Xg_vertices2D):
×
171
    """
172
    Add faces and vertices to the X matrix.
173
    :param X:
174
    :param Xg_faceCentres2D:
175
    :param Xg_vertices2D:
176
    :return:
177
    """
178
    Xg_nodes = np.vstack((Xg_faceCentres2D, Xg_vertices2D))
×
UNCOV
179
    Xg_ids = np.arange(X.shape[0] + 1, X.shape[0] + Xg_nodes.shape[0] + 1)
×
UNCOV
180
    Xg_faceIds = Xg_ids[0:Xg_faceCentres2D.shape[0]]
×
UNCOV
181
    Xg_verticesIds = Xg_ids[Xg_faceCentres2D.shape[0]:]
×
UNCOV
182
    X = np.vstack((X, Xg_nodes))
×
UNCOV
183
    return X, Xg_faceIds, Xg_ids, Xg_verticesIds
×
184

185

UNCOV
186
def create_tetrahedra(triangles_connectivity, neighbours_network, edges_of_vertices, x_internal, x_face_ids,
×
187
                      x_vertices_ids):
188
    """
189
    Add connections between real nodes and ghost cells to create tetrahedra.
190

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

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

204
    # Relationships: 1 cell node and 3 ghost nodes
UNCOV
205
    new_additions = []
×
206
    for id_cell, num_cell in enumerate(x_internal):
×
207
        face_id = x_face_ids[id_cell]
×
UNCOV
208
        vertices_to_connect = edges_of_vertices[id_cell]
×
209
        new_additions.extend(np.hstack([np.repeat(np.array([[num_cell, face_id]]), len(vertices_to_connect), axis=0),
×
210
                                        x_vertices_ids[vertices_to_connect]]))
211
    twg = np.vstack([twg, new_additions])
×
212

213
    # Relationships: 2 ghost nodes, two cell nodes
214
    twg_sorted = np.sort(twg[np.any(np.isin(twg, x_ids), axis=1)], axis=1)
×
215
    internal_neighbour_network = [neighbour for neighbour in neighbours_network if
×
216
                                  np.any(np.isin(neighbour, x_internal))]
217
    internal_neighbour_network = np.unique(np.sort(internal_neighbour_network, axis=1), axis=0)
×
218

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

UNCOV
231
    return twg
×
232

233

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

247

UNCOV
248
class VertexModel:
×
249
    """
250
    The main class for the vertex model simulation. It contains the methods for initializing the model,
251
    iterating over time, applying Brownian motion, and checking the integrity of the model.
252
    """
253

254
    def __init__(self, c_set=None, create_output_folder=True, update_derived_parameters=True):
×
255
        """
256
        Vertex Model class.
257
        :param c_set:
258
        """
259
        self.colormap_lim = None
×
260
        self.OutputFolder = None
×
261
        self.numStep = None
×
UNCOV
262
        self.backupVars = None
×
UNCOV
263
        self.geo_n = None
×
264
        self.geo_0 = None
×
265
        self.tr = None
×
UNCOV
266
        self.t = None
×
UNCOV
267
        self.X = None
×
268
        self.didNotConverge = False
×
UNCOV
269
        self.geo = Geo()
×
270

271
        # Set definition
272
        if c_set is not None:
×
UNCOV
273
            self.set = c_set
×
274
        else:
275
            # TODO Create a menu to select the set
UNCOV
276
            self.set = Set()
×
277
            # self.set.cyst()
278
            self.set.wing_disc()
×
279
            if self.set.ablation:
×
UNCOV
280
                self.set.wound_default()
×
281

282
            if update_derived_parameters:
×
UNCOV
283
                self.set.update_derived_parameters()
×
284

285
        # Redirect output
286
        if self.set.OutputFolder is not None and create_output_folder:
×
287
            self.set.redirect_output()
×
288

289
        # Degrees of freedom definition
290
        self.Dofs = degreesOfFreedom.DegreesOfFreedom()
×
291

292
        self.relaxingNu = False
×
UNCOV
293
        self.EnergiesPerTimeStep = []
×
294
        self.t = 0
×
UNCOV
295
        self.tr = 0
×
UNCOV
296
        self.numStep = 1
×
297

UNCOV
298
    @abstractmethod
×
UNCOV
299
    def initialize(self):
×
UNCOV
300
        pass
×
301

UNCOV
302
    def brownian_motion(self, scale):
×
303
        """
304
        Applies Brownian motion to the vertices of cells in the Geo structure.
305
        Displacements are generated with a normal distribution in each dimension.
306
        :param scale:
307
        :return:
308
        """
309

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

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

318
        # Update vertex positions based on 3D Brownian motion displacements
UNCOV
319
        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
320
            _, corresponding_ids = np.where(np.all(np.sort(cell.T, axis=1)[:, None] == all_tets_unique, axis=2))
×
321
            cell.Y += displacements[corresponding_ids, :]
×
322

323
    def iterate_over_time(self):
×
324
        """
325
        Iterate the model over time. This includes updating the degrees of freedom, applying boundary conditions,
326
        updating measures, and checking for convergence.
327
        :return:
328
        """
UNCOV
329
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
330
        if not os.path.exists(temp_dir):
×
331
            os.makedirs(temp_dir)
×
332

UNCOV
333
        if self.set.Substrate == 1:
×
334
            self.Dofs.GetDOFsSubstrate(self.geo, self.set)
×
335
        else:
UNCOV
336
            self.Dofs.get_dofs(self.geo, self.set)
×
337

UNCOV
338
        self.geo.remodelling = False
×
339
        if self.geo_0 is None:
×
340
            self.geo_0 = self.geo.copy(update_measurements=False)
×
341

342
        if self.geo_n is None:
×
343
            self.geo_n = self.geo.copy(update_measurements=False)
×
344

345
        self.backupVars = save_backup_vars(self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs)
×
346

UNCOV
347
        print("File: ", self.set.OutputFolder)
×
348
        self.save_v_model_state()
×
349

350
        while self.t <= self.set.tend and not self.didNotConverge:
×
UNCOV
351
            gr = self.single_iteration()
×
352

UNCOV
353
            if np.isnan(gr):
×
UNCOV
354
                break
×
355

356
        return self.didNotConverge
×
357

358
    def single_iteration(self, post_operations=True):
×
359
        """
360
        Perform a single iteration of the model.
361
        :return:
362
        """
363
        self.set.currentT = self.t
×
364
        logger.info("Time: " + str(self.t))
×
365
        if not self.relaxingNu:
×
UNCOV
366
            self.set.i_incr = self.numStep
×
367

368
            # Ablate cells if needed
369
            if self.set.ablation:
×
UNCOV
370
                if self.set.ablation and self.set.TInitAblation <= self.t and self.geo.cellsToAblate is not None:
×
UNCOV
371
                    self.save_v_model_state(file_name='before_ablation')
×
372
                self.geo.ablate_cells(self.set, self.t)
×
373
                self.geo_n = self.geo.copy()
×
374
                # Update the degrees of freedom
UNCOV
375
                self.Dofs.get_dofs(self.geo, self.set)
×
376

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

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

417
        self.Dofs.get_dofs(self.geo, self.set)
×
418

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

UNCOV
439
    def iteration_converged(self):
×
440
        """
441
        If the iteration converged, the algorithm will update the values of the variables and proceed to the next step.
442
        :return:
443
        """
444
        if self.set.nu / self.set.nu0 == 1:
×
445
            # STEP has converged
446
            logger.info(f"STEP {str(self.set.i_incr)} has converged ...")
×
447

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

451
            # Remodelling
452
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
UNCOV
453
                if self.set.Remodelling:
×
454
                    save_state(self,
×
455
                               os.path.join(self.set.OutputFolder,
456
                                            'data_step_before_remodelling_' + str(self.numStep) + '.pkl'))
457

458
                    # Remodelling
UNCOV
459
                    remodel_obj = Remodelling(self.geo, self.geo_n, self.geo_0, self.set, self.Dofs)
×
UNCOV
460
                    self.geo, self.geo_n = remodel_obj.remodel_mesh(self.numStep)
×
461
                    # Update tolerance if remodelling was performed to the current one
UNCOV
462
                    if self.set.implicit_method is False:
×
UNCOV
463
                        g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
464
                                                            self.set.implicit_method)
UNCOV
465
                        self.Dofs.get_dofs(self.geo, self.set)
×
UNCOV
466
                        gr = np.linalg.norm(g[self.Dofs.Free])
×
467

468
            # Update last time converged
UNCOV
469
            self.set.last_t_converged = self.t
×
470

471
            # Test Geo
472
            # TODO: CHECK
473
            # self.check_integrity()
474

UNCOV
475
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
UNCOV
476
                self.save_v_model_state()
×
477

478
                # Reset noise to be comparable between simulations
UNCOV
479
                self.reset_noisy_parameters()
×
480
                # Count the number of faces in average has a cell per domain
481
                self.geo.update_barrier_tri0_based_on_number_of_faces()
×
482
                self.tr = self.t
×
483

484
                # Brownian Motion
UNCOV
485
                if self.set.brownian_motion is True:
×
UNCOV
486
                    self.brownian_motion(self.set.brownian_motion_scale)
×
487

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

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

522
    def reset_noisy_parameters(self):
×
523
        """
524
        Reset noisy parameters.
525
        :return:
526
        """
527
        for cell in self.geo.Cells:
×
UNCOV
528
            cell.lambda_s1_perc = add_noise_to_parameter(1, self.set.noise_random)
×
529
            cell.lambda_s2_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
530
            cell.lambda_s3_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
531
            cell.lambda_v_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
532
            cell.lambda_r_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
533
            cell.c_line_tension_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
534
            cell.k_substrate_perc = add_noise_to_parameter(1, self.set.noise_random)
×
UNCOV
535
            cell.lambda_b_perc = add_noise_to_parameter(1, self.set.noise_random)
×
536

537
    def check_integrity(self):
×
538
        """
539
        Performs tests on the properties of cells, faces, and triangles (tris) within the Geo structure.
540
        Ensures that certain geometrical properties are above minimal threshold values.
541
        """
542

543
        # Define minimum error thresholds for edge length, area, and volume
UNCOV
544
        min_error_edge = 1e-5
×
UNCOV
545
        min_error_area = min_error_edge ** 2
×
546
        min_error_volume = min_error_edge ** 3
×
547

548
        # Test Cells properties:
549
        # Conditions checked:
550
        # - Volume > minimum error volume
551
        # - Initial Volume > minimum error volume
552
        # - Area > minimum error area
553
        # - Initial Area > minimum error area
UNCOV
554
        for c_cell in self.geo.Cells:
×
UNCOV
555
            if c_cell.AliveStatus:
×
UNCOV
556
                assert c_cell.Vol > min_error_volume, "Cell volume is too low"
×
557
                assert c_cell.Vol0 > min_error_volume, "Cell initial volume is too low"
×
558
                assert c_cell.Area > min_error_area, "Cell area is too low"
×
559
                assert c_cell.Area0 > min_error_area, "Cell initial area is too low"
×
560

561
        # Test Faces properties:
562
        # Conditions checked:
563
        # - Area > minimum error area
564
        # - Initial Area > minimum error area
UNCOV
565
        for c_cell in self.geo.Cells:
×
UNCOV
566
            if c_cell.AliveStatus:
×
UNCOV
567
                for face in c_cell.Faces:
×
568
                    assert face.Area > min_error_area, "Face area is too low"
×
569
                    assert face.Area0 > min_error_area, "Face initial area is too low"
×
570

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

UNCOV
585
    def analyse_vertex_model(self):
×
586
        """
587
        Analyse the vertex model.
588
        :return:
589
        """
590
        # Initialize average cell properties
UNCOV
591
        cell_features = []
×
592
        debris_features = []
×
593

594
        wound_centre, debris_cells = self.geo.compute_wound_centre()
×
595
        list_of_cell_distances = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=None)
×
596
        list_of_cell_distances_top = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=0)
×
UNCOV
597
        list_of_cell_distances_bottom = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=2)
×
598

599
        # Analyse the alive cells
600
        for cell_id, cell in enumerate(self.geo.Cells):
×
601
            if cell.AliveStatus:
×
602
                cell_features.append(cell.compute_features(wound_centre))
×
603
            elif cell.AliveStatus is not None:
×
604
                debris_features.append(cell.compute_features())
×
605

606
        # Calculate average of cell features
607
        all_cell_features = pd.DataFrame(cell_features)
×
608
        all_cell_features["cell_distance_to_wound"] = list_of_cell_distances
×
609
        all_cell_features["cell_distance_to_wound_top"] = list_of_cell_distances_top
×
610
        all_cell_features["cell_distance_to_wound_bottom"] = list_of_cell_distances_bottom
×
611
        all_cell_features["time"] = self.t
×
UNCOV
612
        avg_cell_features = all_cell_features.mean()
×
613

614
        # Compute wound features
615
        try:
×
UNCOV
616
            wound_features = self.compute_wound_features()
×
UNCOV
617
            avg_cell_features = pd.concat([avg_cell_features, pd.Series(wound_features)])
×
UNCOV
618
        except Exception as e:
×
UNCOV
619
            pass
×
620

UNCOV
621
        return all_cell_features, avg_cell_features
×
622

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

642
        return wound_features
×
643

644
    def copy(self):
×
645
        """
646
        Copy the VertexModel object.
647
        :return:
648
        """
UNCOV
649
        new_v_model = VertexModel()
×
UNCOV
650
        copy_non_mutable_attributes(self, '', new_v_model)
×
651

UNCOV
652
        return new_v_model
×
653

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

665
        # Check if the simulation reached the end
666
        if self.t < self.set.tend:
×
667
            error += (self.t - self.set.tend) ** 2
×
668

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

680
        # Check how similar the recoil from in vivo is to the initial recoil and K value
681
        correct_K = 0.126
×
682
        correct_initial_recoil = 0.213
×
683
        if error_type == 'None' or 'K' in error_type:
×
684
            try:
×
685
                error += np.abs(K[0] - correct_K) * 100
×
UNCOV
686
            except IndexError:
×
687
                error += np.abs(K - correct_K) * 100
×
688

UNCOV
689
        if error_type == 'None' or 'InitialRecoil' in error_type:
×
UNCOV
690
            try:
×
UNCOV
691
                error += np.abs(initial_recoil[0] - correct_initial_recoil) * 100
×
UNCOV
692
            except IndexError:
×
UNCOV
693
                error += np.abs(initial_recoil - correct_initial_recoil) * 100
×
694

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