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

Pablo1990 / pyVertexModel / 12137699613

03 Dec 2024 10:26AM UTC coverage: 0.773% (-0.002%) from 0.775%
12137699613

push

github

Pablo1990
screenshot available for not vertex models

0 of 1043 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 67 new or added lines in 3 files covered. (0.0%)

4 existing lines in 3 files now uncovered.

51 of 5553 relevant lines covered (0.92%)

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 numpy as np
×
7
import pandas as pd
×
8
from skimage.measure import regionprops
×
9

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

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

UNCOV
21
def generate_tetrahedra_from_information(X, cell_edges, cell_height, cell_centroids, main_cells,
×
22
                                         neighbours_network, selected_planes, triangles_connectivity,
23
                                         vertices_of_cell_pos, geo):
24
    """
25
    Generate tetrahedra from the information of the cells.
26
    :param X:
27
    :param cell_edges:
28
    :param cell_height:
29
    :param cell_centroids:
30
    :param main_cells:
31
    :param neighbours_network:
32
    :param selected_planes:
33
    :param triangles_connectivity:
34
    :param vertices_of_cell_pos:
35
    :param geo:
36
    :return:
37
    """
38
    bottom_plane = 0
×
39
    top_plane = 1
×
40
    if bottom_plane == 0:
×
41
        z_coordinate = [-cell_height, cell_height]
×
42
    else:
43
        z_coordinate = [cell_height, -cell_height]
×
44
    Twg = []
×
45
    for idPlane, numPlane in enumerate(selected_planes):
×
46
        # Using the centroids and vertices of the cells of each 2D image as ghost nodes
47
        X, Xg_faceIds, Xg_ids, Xg_verticesIds = (
×
48
            add_faces_and_vertices_to_x(X,
49
                                        np.hstack((cell_centroids[numPlane][:, 1:3], np.tile(z_coordinate[idPlane],
50
                                                                                             (len(cell_centroids[
51
                                                                                                      numPlane][:,
52
                                                                                                  1:3]), 1)))),
53
                                        np.hstack((np.fliplr(vertices_of_cell_pos[numPlane]),
54
                                                   np.tile(z_coordinate[idPlane],
55
                                                           (len(vertices_of_cell_pos[numPlane]), 1))))))
56

57
        # Fill Geo info
58
        if idPlane == bottom_plane:
×
59
            geo.XgBottom = Xg_ids
×
60
        elif idPlane == top_plane:
×
61
            geo.XgTop = Xg_ids
×
62

63
        # Create tetrahedra
64
        Twg_numPlane = create_tetrahedra(triangles_connectivity[numPlane], neighbours_network[numPlane],
×
65
                                         cell_edges[numPlane], main_cells, Xg_faceIds, Xg_verticesIds)
66

67
        Twg.append(Twg_numPlane)
×
68
    Twg = np.vstack(Twg)
×
69
    return Twg, X
×
70

71

72
def add_faces_and_vertices_to_x(X, Xg_faceCentres2D, Xg_vertices2D):
×
73
    """
74
    Add faces and vertices to the X matrix.
75
    :param X:
76
    :param Xg_faceCentres2D:
77
    :param Xg_vertices2D:
78
    :return:
79
    """
80
    Xg_nodes = np.vstack((Xg_faceCentres2D, Xg_vertices2D))
×
81
    Xg_ids = np.arange(X.shape[0] + 1, X.shape[0] + Xg_nodes.shape[0] + 1)
×
82
    Xg_faceIds = Xg_ids[0:Xg_faceCentres2D.shape[0]]
×
83
    Xg_verticesIds = Xg_ids[Xg_faceCentres2D.shape[0]:]
×
84
    X = np.vstack((X, Xg_nodes))
×
85
    return X, Xg_faceIds, Xg_ids, Xg_verticesIds
×
86

87

88
def create_tetrahedra(triangles_connectivity, neighbours_network, edges_of_vertices, x_internal, x_face_ids,
×
89
                      x_vertices_ids):
90
    """
91
    Add connections between real nodes and ghost cells to create tetrahedra.
92

93
    :param triangles_connectivity: A 2D array where each row represents a triangle connectivity.
94
    :param neighbours_network: A 2D array where each row represents a pair of neighboring nodes.
95
    :param edges_of_vertices: A list of lists where each sublist represents the edges of a vertex.
96
    :param x_internal: A 1D array representing the internal nodes.
97
    :param x_face_ids: A 1D array representing the face ids.
98
    :param x_vertices_ids: A 1D array representing the vertices ids.
99
    :return: A 2D array representing the tetrahedra.
100
    """
101
    x_ids = np.concatenate([x_face_ids, x_vertices_ids])
×
102

103
    # Relationships: 1 ghost node, three cell nodes
104
    twg = np.hstack([triangles_connectivity, x_vertices_ids[:, None]])
×
105

106
    # Relationships: 1 cell node and 3 ghost nodes
107
    new_additions = []
×
108
    for id_cell, num_cell in enumerate(x_internal):
×
109
        face_id = x_face_ids[id_cell]
×
110
        vertices_to_connect = edges_of_vertices[id_cell]
×
111
        new_additions.extend(np.hstack([np.repeat(np.array([[num_cell, face_id]]), len(vertices_to_connect), axis=0),
×
112
                                        x_vertices_ids[vertices_to_connect]]))
113
    twg = np.vstack([twg, new_additions])
×
114

115
    # Relationships: 2 ghost nodes, two cell nodes
116
    twg_sorted = np.sort(twg[np.any(np.isin(twg, x_ids), axis=1)], axis=1)
×
117
    internal_neighbour_network = [neighbour for neighbour in neighbours_network if
×
118
                                  np.any(np.isin(neighbour, x_internal))]
119
    internal_neighbour_network = np.unique(np.sort(internal_neighbour_network, axis=1), axis=0)
×
120

121
    new_additions = []
×
122
    for num_pair in range(internal_neighbour_network.shape[0]):
×
123
        found = np.isin(twg_sorted, internal_neighbour_network[num_pair])
×
124
        new_connections = np.unique(twg_sorted[np.sum(found, axis=1) == 2, 3])
×
125
        if len(new_connections) > 1:
×
126
            new_connections_pairs = np.array(list(combinations(new_connections, 2)))
×
127
            new_additions.extend([np.hstack([internal_neighbour_network[num_pair], new_connections_pair])
×
128
                                  for new_connections_pair in new_connections_pairs])
129
        else:
130
            raise ValueError('Error while creating the connections and initial topology')
×
131
    twg = np.vstack([twg, new_additions])
×
132

133
    return twg
×
134

135

136
def calculate_cell_height_on_model(img2DLabelled, main_cells, c_set):
×
137
    """
138
    Calculate the cell height on the model regarding the diameter of the cells.
139
    :param img2DLabelled:
140
    :param main_cells:
141
    :return:
142
    """
143
    properties = regionprops(img2DLabelled)
×
144
    # Extract major axis lengths
145
    avg_diameter = np.mean([prop.major_axis_length for prop in properties if prop.label in main_cells])
×
146
    cell_height = avg_diameter * c_set.CellHeight
×
147
    return cell_height
×
148

149

150
class VertexModel:
×
151
    """
152
    The main class for the vertex model simulation. It contains the methods for initializing the model,
153
    iterating over time, applying Brownian motion, and checking the integrity of the model.
154
    """
155

156
    def __init__(self, c_set=None, create_output_folder=True, update_derived_parameters=True):
×
157
        """
158
        Vertex Model class.
159
        :param c_set:
160
        """
161
        self.colormap_lim = None
×
162
        self.OutputFolder = None
×
163
        self.numStep = None
×
164
        self.backupVars = None
×
165
        self.geo_n = None
×
166
        self.geo_0 = None
×
167
        self.tr = None
×
168
        self.t = None
×
169
        self.X = None
×
170
        self.didNotConverge = False
×
171
        self.geo = Geo()
×
172

173
        # Set definition
174
        if c_set is not None:
×
175
            self.set = c_set
×
176
        else:
177
            # TODO Create a menu to select the set
178
            self.set = Set()
×
179
            # self.set.cyst()
180
            self.set.wing_disc()
×
181
            if self.set.ablation:
×
182
                self.set.wound_default()
×
183

184
            if update_derived_parameters:
×
185
                self.set.update_derived_parameters()
×
186

187
        # Redirect output
188
        if self.set.OutputFolder is not None and create_output_folder:
×
189
            self.set.redirect_output()
×
190

191
        # Degrees of freedom definition
192
        self.Dofs = degreesOfFreedom.DegreesOfFreedom()
×
193

194
        self.relaxingNu = False
×
195
        self.EnergiesPerTimeStep = []
×
196
        self.t = 0
×
197
        self.tr = 0
×
198
        self.numStep = 1
×
199

200
    @abstractmethod
×
201
    def initialize(self):
×
202
        pass
×
203

204
    def brownian_motion(self, scale):
×
205
        """
206
        Applies Brownian motion to the vertices of cells in the Geo structure.
207
        Displacements are generated with a normal distribution in each dimension.
208
        :param scale:
209
        :return:
210
        """
211

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

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

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

225
    def iterate_over_time(self):
×
226
        """
227
        Iterate the model over time. This includes updating the degrees of freedom, applying boundary conditions,
228
        updating measures, and checking for convergence.
229
        :return:
230
        """
231
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
232
        if not os.path.exists(temp_dir):
×
233
            os.makedirs(temp_dir)
×
234

235
        if self.set.Substrate == 1:
×
236
            self.Dofs.GetDOFsSubstrate(self.geo, self.set)
×
237
        else:
238
            self.Dofs.get_dofs(self.geo, self.set)
×
239

240
        self.geo.remodelling = False
×
241
        if self.geo_0 is None:
×
242
            self.geo_0 = self.geo.copy(update_measurements=False)
×
243

244
        if self.geo_n is None:
×
245
            self.geo_n = self.geo.copy(update_measurements=False)
×
246

247
        self.backupVars = save_backup_vars(self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs)
×
248

249
        print("File: ", self.set.OutputFolder)
×
250
        self.save_v_model_state()
×
251

252
        while self.t <= self.set.tend and not self.didNotConverge:
×
253
            gr = self.single_iteration()
×
254

255
            if np.isnan(gr):
×
256
                break
×
257

258
        return self.didNotConverge
×
259

260
    def single_iteration(self, post_operations=True):
×
261
        """
262
        Perform a single iteration of the model.
263
        :return:
264
        """
265
        self.set.currentT = self.t
×
266
        logger.info("Time: " + str(self.t))
×
267
        if not self.relaxingNu:
×
268
            self.set.i_incr = self.numStep
×
269

270
            # Ablate cells if needed
271
            if self.set.ablation:
×
272
                if self.set.ablation and self.set.TInitAblation <= self.t and self.geo.cellsToAblate is not None:
×
273
                    self.save_v_model_state(file_name='before_ablation')
×
274
                self.geo.ablate_cells(self.set, self.t)
×
275
                self.geo_n = self.geo.copy()
×
276
                # Update the degrees of freedom
277
                self.Dofs.get_dofs(self.geo, self.set)
×
278

279
            self.Dofs.ApplyBoundaryCondition(self.t, self.geo, self.set)
×
280
            # IMPORTANT: Here it updates: Areas, Volumes, etc... Should be
281
            # up-to-date
282
            self.geo.update_measures()
×
283
        if self.set.implicit_method is True:
×
284
            g, K, _, energies = newtonRaphson.KgGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
285
                                                       self.set.implicit_method)
286
        else:
287
            K = 0
×
288
            g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
289
                                                self.set.implicit_method)
290
        for key, energy in energies.items():
×
291
            logger.info(f"{key}: {energy}")
×
292
        self.geo, g, __, __, self.set, gr, dyr, dy = newtonRaphson.newton_raphson(self.geo_0, self.geo_n, self.geo,
×
293
                                                                                  self.Dofs, self.set, K, g,
294
                                                                                  self.numStep, self.t,
295
                                                                                  self.set.implicit_method)
296
        if not np.isnan(gr) and post_operations:
×
297
            self.post_newton_raphson(dy, dyr, g, gr)
×
298
        return gr
×
299

300
    def post_newton_raphson(self, dy, dyr, g, gr):
×
301
        """
302
        Post Newton Raphson operations.
303
        :param dy:
304
        :param dyr:
305
        :param g:
306
        :param gr:
307
        :return:
308
        """
309
        if (gr < self.set.tol and dyr < self.set.tol and np.all(~np.isnan(g[self.Dofs.Free])) and
×
310
                np.all(~np.isnan(dy[self.Dofs.Free]))):
311
            self.iteration_converged()
×
312
            # if self.set.implicit_method is False:
313
            #     self.set.tol = gr
314
            #     if self.set.tol < self.set.tol0:
315
            #         self.set.tol = self.set.tol0
316
        else:
317
            self.iteration_did_not_converged()
×
318

319
        self.Dofs.get_dofs(self.geo, self.set)
×
320

321
    def iteration_did_not_converged(self):
×
322
        """
323
        If the iteration did not converge, the algorithm will try to relax the value of nu and dt.
324
        :return:
325
        """
326
        self.geo, self.geo_n, self.geo_0, self.tr, self.Dofs = load_backup_vars(self.backupVars)
×
327
        self.relaxingNu = False
×
328
        if self.set.iter == self.set.MaxIter0 and self.set.implicit_method:
×
329
            self.set.MaxIter = self.set.MaxIter0 * 3
×
330
            self.set.nu = 10 * self.set.nu0
×
331
        else:
332
            if (self.set.iter >= self.set.MaxIter and
×
333
                    (self.set.dt / self.set.dt0) > 1e-6):
334
                self.set.MaxIter = self.set.MaxIter0
×
335
                self.set.nu = self.set.nu0
×
336
                self.set.dt = self.set.dt / 2
×
337
                self.t = self.set.last_t_converged + self.set.dt
×
338
            else:
339
                self.didNotConverge = True
×
340

341
    def iteration_converged(self):
×
342
        """
343
        If the iteration converged, the algorithm will update the values of the variables and proceed to the next step.
344
        :return:
345
        """
346
        if self.set.nu / self.set.nu0 == 1:
×
347
            # STEP has converged
348
            logger.info(f"STEP {str(self.set.i_incr)} has converged ...")
×
349

350
            # Build X From Y
351
            self.geo.build_x_from_y(self.geo_n)
×
352

353
            # Remodelling
354
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
355
                if self.set.Remodelling:
×
356
                    save_state(self,
×
357
                               os.path.join(self.set.OutputFolder,
358
                                            'data_step_before_remodelling_' + str(self.numStep) + '.pkl'))
359

360
                    # Remodelling
361
                    remodel_obj = Remodelling(self.geo, self.geo_n, self.geo_0, self.set, self.Dofs)
×
362
                    self.geo, self.geo_n = remodel_obj.remodel_mesh(self.numStep)
×
363
                    # Update tolerance if remodelling was performed to the current one
364
                    if self.set.implicit_method is False:
×
365
                        g, energies = newtonRaphson.gGlobal(self.geo_0, self.geo_n, self.geo, self.set,
×
366
                                                            self.set.implicit_method)
367
                        self.Dofs.get_dofs(self.geo, self.set)
×
368
                        gr = np.linalg.norm(g[self.Dofs.Free])
×
369

370
            # Update last time converged
371
            self.set.last_t_converged = self.t
×
372

373
            # Test Geo
374
            # TODO: CHECK
375
            # self.check_integrity()
376

377
            if abs(self.t - self.tr) >= self.set.RemodelingFrequency:
×
378
                self.save_v_model_state()
×
379

380
                # Reset noise to be comparable between simulations
381
                self.reset_noisy_parameters()
×
382
                # Count the number of faces in average has a cell per domain
383
                self.geo.update_barrier_tri0_based_on_number_of_faces()
×
384
                self.tr = self.t
×
385

386
                # Brownian Motion
387
                if self.set.brownian_motion is True:
×
388
                    self.brownian_motion(self.set.brownian_motion_scale)
×
389

390
            self.t = self.t + self.set.dt
×
391
            self.set.dt = np.min([self.set.dt + self.set.dt * 0.5, self.set.dt0])
×
392
            self.set.MaxIter = self.set.MaxIter0
×
393
            self.numStep = self.numStep + 1
×
394
            self.backupVars = {
×
395
                'Geo_b': self.geo.copy(),
396
                'Geo_n_b': self.geo_n.copy(),
397
                'Geo_0_b': self.geo_0.copy(),
398
                'tr_b': self.tr,
399
                'Dofs': self.Dofs.copy()
400
            }
401
            self.geo_n = self.geo.copy()
×
402
            self.relaxingNu = False
×
403
        else:
404
            self.set.nu = np.max([self.set.nu / 2, self.set.nu0])
×
405
            self.relaxingNu = True
×
406

407
    def save_v_model_state(self, file_name=None):
×
408
        """
409
        Save the state of the vertex model.
410
        :param file_name:
411
        :return:
412
        """
413
        # Create VTK files for the current state
414
        self.geo.create_vtk_cell(self.set, self.numStep, 'Edges')
×
415
        self.geo.create_vtk_cell(self.set, self.numStep, 'Cells')
×
416
        temp_dir = os.path.join(self.set.OutputFolder, 'images')
×
417
        screenshot(self, temp_dir)
×
418
        # Save Data of the current step
419
        if file_name is None:
×
420
            save_state(self, os.path.join(self.set.OutputFolder, 'data_step_' + str(self.numStep) + '.pkl'))
×
421
        else:
422
            save_state(self, os.path.join(self.set.OutputFolder, file_name + '.pkl'))
×
423

424
    def reset_noisy_parameters(self):
×
425
        """
426
        Reset noisy parameters.
427
        :return:
428
        """
429
        for cell in self.geo.Cells:
×
430
            cell.lambda_s1_perc = add_noise_to_parameter(1, self.set.noise_random)
×
431
            cell.lambda_s2_perc = add_noise_to_parameter(1, self.set.noise_random)
×
432
            cell.lambda_s3_perc = add_noise_to_parameter(1, self.set.noise_random)
×
433
            cell.lambda_v_perc = add_noise_to_parameter(1, self.set.noise_random)
×
434
            cell.lambda_r_perc = add_noise_to_parameter(1, self.set.noise_random)
×
435
            cell.c_line_tension_perc = add_noise_to_parameter(1, self.set.noise_random)
×
436
            cell.k_substrate_perc = add_noise_to_parameter(1, self.set.noise_random)
×
437
            cell.lambda_b_perc = add_noise_to_parameter(1, self.set.noise_random)
×
438

439
    def check_integrity(self):
×
440
        """
441
        Performs tests on the properties of cells, faces, and triangles (tris) within the Geo structure.
442
        Ensures that certain geometrical properties are above minimal threshold values.
443
        """
444

445
        # Define minimum error thresholds for edge length, area, and volume
446
        min_error_edge = 1e-5
×
447
        min_error_area = min_error_edge ** 2
×
448
        min_error_volume = min_error_edge ** 3
×
449

450
        # Test Cells properties:
451
        # Conditions checked:
452
        # - Volume > minimum error volume
453
        # - Initial Volume > minimum error volume
454
        # - Area > minimum error area
455
        # - Initial Area > minimum error area
456
        for c_cell in self.geo.Cells:
×
457
            if c_cell.AliveStatus:
×
458
                assert c_cell.Vol > min_error_volume, "Cell volume is too low"
×
459
                assert c_cell.Vol0 > min_error_volume, "Cell initial volume is too low"
×
460
                assert c_cell.Area > min_error_area, "Cell area is too low"
×
461
                assert c_cell.Area0 > min_error_area, "Cell initial area is too low"
×
462

463
        # Test Faces properties:
464
        # Conditions checked:
465
        # - Area > minimum error area
466
        # - Initial Area > minimum error area
467
        for c_cell in self.geo.Cells:
×
468
            if c_cell.AliveStatus:
×
469
                for face in c_cell.Faces:
×
470
                    assert face.Area > min_error_area, "Face area is too low"
×
471
                    assert face.Area0 > min_error_area, "Face initial area is too low"
×
472

473
        # Test Tris properties:
474
        # Conditions checked:
475
        # - Edge length > minimum error edge length
476
        # - Any Lengths to Centre > minimum error edge length
477
        # - Area > minimum error area
478
        for c_cell in self.geo.Cells:
×
479
            if c_cell.AliveStatus:
×
480
                for face in c_cell.Faces:
×
481
                    for tris in face.Tris:
×
482
                        assert tris.EdgeLength > min_error_edge, "Triangle edge length is too low"
×
483
                        assert any(length > min_error_edge for length in
×
484
                                   tris.LengthsToCentre), "Triangle lengths to centre are too low"
485
                        assert tris.Area > min_error_area, "Triangle area is too low"
×
486

487
    def analyse_vertex_model(self):
×
488
        """
489
        Analyse the vertex model.
490
        :return:
491
        """
492
        # Initialize average cell properties
493
        cell_features = []
×
494
        debris_features = []
×
495

496
        wound_centre, debris_cells = self.geo.compute_wound_centre()
×
497
        list_of_cell_distances = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=None)
×
498
        list_of_cell_distances_top = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=0)
×
499
        list_of_cell_distances_bottom = self.geo.compute_cell_distance_to_wound(debris_cells, location_filter=2)
×
500

501
        # Analyse the alive cells
502
        for cell_id, cell in enumerate(self.geo.Cells):
×
503
            if cell.AliveStatus:
×
504
                cell_features.append(cell.compute_features(wound_centre))
×
505
            elif cell.AliveStatus is not None:
×
506
                debris_features.append(cell.compute_features())
×
507

508
        # Calculate average of cell features
509
        all_cell_features = pd.DataFrame(cell_features)
×
510
        all_cell_features["cell_distance_to_wound"] = list_of_cell_distances
×
511
        all_cell_features["cell_distance_to_wound_top"] = list_of_cell_distances_top
×
512
        all_cell_features["cell_distance_to_wound_bottom"] = list_of_cell_distances_bottom
×
513
        all_cell_features["time"] = self.t
×
514
        avg_cell_features = all_cell_features.mean()
×
515

516
        # Compute wound features
517
        #try:
518
        wound_features = self.compute_wound_features()
×
519
        avg_cell_features = pd.concat([avg_cell_features, pd.Series(wound_features)])
×
520
        # except Exception as e:
521
        #     logger.error(f"Error while computing wound features: {e}")
522

523
        return all_cell_features, avg_cell_features
×
524

525
    def compute_wound_features(self):
×
526
        """
527
        Compute wound features.
528
        :return:
529
        """
530
        wound_features = {
×
531
            'num_cells_wound_edge': len(self.geo.compute_cells_wound_edge()),
532
            'num_cells_wound_edge_top': len(self.geo.compute_cells_wound_edge(location_filter="Top")),
533
            'num_cells_wound_edge_bottom': len(self.geo.compute_cells_wound_edge(location_filter="Bottom")),
534
            'wound_area_top': self.geo.compute_wound_area(location_filter="Top"),
535
            'wound_area_bottom': self.geo.compute_wound_area(location_filter="Bottom"),
536
            'wound_volume': self.geo.compute_wound_volume(),
537
            'wound_height': self.geo.compute_wound_height(),
538
            'wound_aspect_ratio_top': self.geo.compute_wound_aspect_ratio(location_filter="Top"),
539
            'wound_aspect_ratio_bottom': self.geo.compute_wound_aspect_ratio(location_filter="Bottom"),
540
            'wound_perimeter_top': self.geo.compute_wound_perimeter(location_filter="Top"),
541
            'wound_perimeter_bottom': self.geo.compute_wound_perimeter(location_filter="Bottom"),
542
            'wound_indentation_top': self.geo.compute_wound_indentation(location_filter="Top"),
543
            'wound_indentation_bottom': self.geo.compute_wound_indentation(location_filter="Bottom"),
544
        }
545

546
        return wound_features
×
547

548
    def copy(self):
×
549
        """
550
        Copy the VertexModel object.
551
        :return:
552
        """
553
        new_v_model = VertexModel()
×
554
        copy_non_mutable_attributes(self, '', new_v_model)
×
555

556
        return new_v_model
×
557

558
    def calculate_error(self, K, initial_recoil, error_type=None):
×
559
        """
560
        Calculate the error of the model.
561
        :return:
562
        """
563
        # The error consist on:
564
        # - There shouldn't be any cells with very small area in the top or bottom domain.
565
        # - It should get until the end of the simulation (tend).
566
        # - When ablating, it should get to: 165 percentage of area at 35.8 minutes.
567
        error = 0
×
568

569
        # Check if the simulation reached the end
570
        if self.t < self.set.tend:
×
571
            error += (self.t - self.set.tend) ** 2
×
572

573
        # # Check how many cells have a very small area
574
        if error_type == 'None' or 'SmallArea' in error_type:
×
575
            std_area_top = np.std([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
576
            std_area_bottom = np.std([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
577
            mean_area_top = np.mean([cell.compute_area(location_filter=0) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
578
            mean_area_bottom = np.mean([cell.compute_area(location_filter=2) for cell in self.geo.Cells if cell.AliveStatus == 1])
×
579
            zscore_area_top = std_area_top / mean_area_top
×
580
            zscore_area_bottom = std_area_bottom / mean_area_bottom
×
581
            error += zscore_area_top ** 2
×
582
            error += zscore_area_bottom ** 2
×
583

584
        # Check how similar the recoil from in vivo is to the initial recoil and K value
585
        correct_K = 0.126
×
586
        correct_initial_recoil = 0.213
×
587
        if error_type == 'None' or 'K' in error_type:
×
588
            try:
×
589
                error += np.abs(K[0] - correct_K) * 100
×
590
            except IndexError:
×
591
                error += np.abs(K - correct_K) * 100
×
592

593
        if error_type == 'None' or 'InitialRecoil' in error_type:
×
594
            try:
×
595
                error += np.abs(initial_recoil[0] - correct_initial_recoil) * 100
×
596
            except IndexError:
×
597
                error += np.abs(initial_recoil - correct_initial_recoil) * 100
×
598

599
        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