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

CCPBioSim / CodeEntropy / 14972158685

12 May 2025 12:25PM UTC coverage: 40.957% (+0.4%) from 40.52%
14972158685

push

github

web-flow
Merge pull request #88 from CCPBioSim/85-oop-refactor

Object-oriented programming Refactor for CodeEntropy

168 of 563 new or added lines in 4 files covered. (29.84%)

274 of 669 relevant lines covered (40.96%)

1.23 hits per line

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

7.48
/CodeEntropy/levels.py
1
import logging
3✔
2

3
import numpy as np
3✔
4

5
logger = logging.getLogger(__name__)
3✔
6

7

8
class LevelManager:
3✔
9
    """
10
    Manages the structural and dynamic levels involved in entropy calculations. This
11
    includes selecting relevant levels, computing axes for translation and rotation,
12
    and handling bead-based representations of molecular systems. Provides utility
13
    methods to extract averaged positions, convert coordinates to spherical systems,
14
    compute weighted forces and torques, and manipulate matrices used in entropy
15
    analysis.
16
    """
17

18
    def __init__(self):
3✔
19
        """
20
        Initializes the LevelManager with placeholders for level-related data,
21
        including translational and rotational axes, number of beads, and a
22
        general-purpose data container.
23
        """
NEW
24
        self.data_container = None
×
NEW
25
        self._levels = None
×
NEW
26
        self._trans_axes = None
×
NEW
27
        self._rot_axes = None
×
NEW
28
        self._number_of_beads = None
×
29

30
    def select_levels(self, data_container):
3✔
31
        """
32
        Function to read input system and identify the number of molecules and
33
        the levels (i.e. united atom, residue and/or polymer) that should be used.
34
        The level refers to the size of the bead (atom or collection of atoms)
35
        that will be used in the entropy calculations.
36

37
        Input
38
        -----
39
        arg_DataContainer : MDAnalysis universe object containing the system of interest
40

41
        Returns
42
        -------
43
        number_molecules : integer
44
        levels : array of strings for each molecule
45
        """
46

47
        # fragments is MDAnalysis terminology for what chemists would call molecules
NEW
48
        number_molecules = len(data_container.atoms.fragments)
×
NEW
49
        logger.debug("The number of molecules is {}.".format(number_molecules))
×
50

NEW
51
        fragments = data_container.atoms.fragments
×
NEW
52
        levels = [[] for _ in range(number_molecules)]
×
53

NEW
54
        for molecule in range(number_molecules):
×
NEW
55
            levels[molecule].append(
×
56
                "united_atom"
57
            )  # every molecule has at least one atom
58

NEW
59
            atoms_in_fragment = fragments[molecule].select_atoms("not name H*")
×
NEW
60
            number_residues = len(atoms_in_fragment.residues)
×
61

NEW
62
            if len(atoms_in_fragment) > 1:
×
NEW
63
                levels[molecule].append("residue")
×
64

NEW
65
                if number_residues > 1:
×
NEW
66
                    levels[molecule].append("polymer")
×
67

NEW
68
        logger.debug(f"levels {levels}")
×
69

NEW
70
        return number_molecules, levels
×
71

72
    def get_matrices(
3✔
73
        self, data_container, level, start, end, step, number_frames, highest_level
74
    ):
75
        """
76
        Function to create the force matrix needed for the transvibrational entropy
77
        calculation and the torque matrix for the rovibrational entropy calculation.
78

79
        Input
80
        -----
81
            data_container : MDAnalysis universe type with the information on the
82
            molecule of interest.
83
            level : string, which of the polymer, residue, or united atom levels
84
            are the matrices for.
85
            start : int, starting frame, default 0 (first frame)
86
            end : int, ending frame, default -1 (last frame)
87
            step : int, step for going through trajectories, default 1
88

89
        Returns
90
        -------
91
            force_matrix : force covariance matrix for transvibrational entropy
92
            torque_matrix : torque convariance matrix for rovibrational entropy
93
        """
94

95
        # Make beads
NEW
96
        list_of_beads = self.get_beads(data_container, level)
×
97

98
        # number of beads and frames in trajectory
NEW
99
        number_beads = len(list_of_beads)
×
100

101
        # initialize force and torque arrays
NEW
102
        weighted_forces = [
×
103
            [0 for x in range(number_frames)] for y in range(number_beads)
104
        ]
NEW
105
        weighted_torques = [
×
106
            [0 for x in range(number_frames)] for y in range(number_beads)
107
        ]
108

109
        # Calculate forces/torques for each bead
NEW
110
        for bead_index in range(number_beads):
×
NEW
111
            for timestep in data_container.trajectory[start:end:step]:
×
112
                # Set up axes
113
                # translation and rotation use different axes
114
                # how the axes are defined depends on the level
NEW
115
                trans_axes, rot_axes = self.get_axes(data_container, level, bead_index)
×
116

117
                # Sort out coordinates, forces, and torques for each atom in the bead
NEW
118
                timestep_index = timestep.frame - start
×
NEW
119
                weighted_forces[bead_index][timestep_index] = self.get_weighted_forces(
×
120
                    data_container, list_of_beads[bead_index], trans_axes, highest_level
121
                )
NEW
122
                weighted_torques[bead_index][timestep_index] = (
×
123
                    self.get_weighted_torques(
124
                        data_container, list_of_beads[bead_index], rot_axes
125
                    )
126
                )
127

128
        # Make covariance matrices - looping over pairs of beads
129
        # list of pairs of indices
NEW
130
        pair_list = [(i, j) for i in range(number_beads) for j in range(number_beads)]
×
131

NEW
132
        force_submatrix = [
×
133
            [0 for x in range(number_beads)] for y in range(number_beads)
134
        ]
NEW
135
        torque_submatrix = [
×
136
            [0 for x in range(number_beads)] for y in range(number_beads)
137
        ]
138

NEW
139
        for i, j in pair_list:
×
140
            # for each pair of beads
141
            # reducing effort because the matrix for [i][j] is the transpose of the one
142
            # for [j][i]
NEW
143
            if i <= j:
×
144
                # calculate the force covariance segment of the matrix
NEW
145
                force_submatrix[i][j] = self.create_submatrix(
×
146
                    weighted_forces[i], weighted_forces[j], number_frames
147
                )
NEW
148
                force_submatrix[j][i] = np.transpose(force_submatrix[i][j])
×
149

150
                # calculate the torque covariance segment of the matrix
NEW
151
                torque_submatrix[i][j] = self.create_submatrix(
×
152
                    weighted_torques[i], weighted_torques[j], number_frames
153
                )
NEW
154
                torque_submatrix[j][i] = np.transpose(torque_submatrix[i][j])
×
155

156
        # use np.block to make submatrices into one matrix
NEW
157
        force_matrix = np.block(
×
158
            [
159
                [force_submatrix[i][j] for j in range(number_beads)]
160
                for i in range(number_beads)
161
            ]
162
        )
163

NEW
164
        torque_matrix = np.block(
×
165
            [
166
                [torque_submatrix[i][j] for j in range(number_beads)]
167
                for i in range(number_beads)
168
            ]
169
        )
170

171
        # fliter zeros to remove any rows/columns that are all zero
NEW
172
        force_matrix = self.filter_zero_rows_columns(force_matrix)
×
NEW
173
        torque_matrix = self.filter_zero_rows_columns(torque_matrix)
×
174

NEW
175
        logger.debug(f"Force Matrix: {force_matrix}")
×
NEW
176
        logger.debug(f"Torque Matrix: {torque_matrix}")
×
177

NEW
178
        return force_matrix, torque_matrix
×
179

180
    def get_dihedrals(self, data_container, level):
3✔
181
        """
182
        Define the set of dihedrals for use in the conformational entropy function.
183
        If residue level, the dihedrals are defined from the atoms
184
        (4 bonded atoms for 1 dihedral).
185
        If polymer level, use the bonds between residues to cast dihedrals.
186
        Note: not using improper dihedrals only ones with 4 atoms/residues
187
        in a linear arrangement.
188

189
        Input
190
        -----
191
        data_container : system information
192
        level : level of the hierarchy (should be residue or polymer)
193

194
        Output
195
        ------
196
        dihedrals : set of dihedrals
197
        """
198
        # Start with empty array
NEW
199
        dihedrals = []
×
200

201
        # if united atom level, read dihedrals from MDAnalysis universe
NEW
202
        if level == "united_atom":
×
NEW
203
            dihedrals = data_container.dihedrals
×
204

205
        # if residue level, looking for dihedrals involving residues
NEW
206
        if level == "residue":
×
NEW
207
            num_residues = len(data_container.residues)
×
NEW
208
            if num_residues < 4:
×
NEW
209
                logger.debug("no residue level dihedrals")
×
210

211
            else:
212
                # find bonds between residues N-3:N-2 and N-1:N
NEW
213
                for residue in range(4, num_residues + 1):
×
214
                    # Using MDAnalysis selection,
215
                    # assuming only one covalent bond between neighbouring residues
216
                    # TODO not written for branched polymers
NEW
217
                    atom_string = (
×
218
                        "resindex "
219
                        + str(residue - 4)
220
                        + " and bonded resindex "
221
                        + str(residue - 3)
222
                    )
NEW
223
                    atom1 = data_container.select_atoms(atom_string)
×
224

NEW
225
                    atom_string = (
×
226
                        "resindex "
227
                        + str(residue - 3)
228
                        + " and bonded resindex "
229
                        + str(residue - 4)
230
                    )
NEW
231
                    atom2 = data_container.select_atoms(atom_string)
×
232

NEW
233
                    atom_string = (
×
234
                        "resindex "
235
                        + str(residue - 2)
236
                        + " and bonded resindex "
237
                        + str(residue - 1)
238
                    )
NEW
239
                    atom3 = data_container.select_atoms(atom_string)
×
240

NEW
241
                    atom_string = (
×
242
                        "resindex "
243
                        + str(residue - 1)
244
                        + " and bonded resindex "
245
                        + str(residue - 2)
246
                    )
NEW
247
                    atom4 = data_container.select_atoms(atom_string)
×
248

NEW
249
                    atom_group = atom1 + atom2 + atom3 + atom4
×
NEW
250
                    dihedrals.append(atom_group.dihedral)
×
251

NEW
252
        logger.debug(f"Dihedrals: {dihedrals}")
×
253

NEW
254
        return dihedrals
×
255

256
    def get_beads(self, data_container, level):
3✔
257
        """
258
        Function to define beads depending on the level in the hierarchy.
259

260
        Input
261
        -----
262
        data_container : the MDAnalysis universe
263
        level : the heirarchy level (polymer, residue, or united atom)
264

265
        Output
266
        ------
267
        list_of_beads : the relevent beads
268
        """
269

NEW
270
        if level == "polymer":
×
NEW
271
            list_of_beads = []
×
NEW
272
            atom_group = "all"
×
NEW
273
            list_of_beads.append(data_container.select_atoms(atom_group))
×
274

NEW
275
        if level == "residue":
×
NEW
276
            list_of_beads = []
×
NEW
277
            num_residues = len(data_container.residues)
×
NEW
278
            for residue in range(num_residues):
×
NEW
279
                atom_group = "resindex " + str(residue)
×
NEW
280
                list_of_beads.append(data_container.select_atoms(atom_group))
×
281

NEW
282
        if level == "united_atom":
×
NEW
283
            list_of_beads = []
×
NEW
284
            heavy_atoms = data_container.select_atoms("not name H*")
×
NEW
285
            for atom in heavy_atoms:
×
NEW
286
                atom_group = (
×
287
                    "index "
288
                    + str(atom.index)
289
                    + " or (name H* and bonded index "
290
                    + str(atom.index)
291
                    + ")"
292
                )
NEW
293
                list_of_beads.append(data_container.select_atoms(atom_group))
×
294

NEW
295
        logger.debug(f"List of beads: {list_of_beads}")
×
296

NEW
297
        return list_of_beads
×
298

299
    def get_axes(self, data_container, level, index=0):
3✔
300
        """
301
        Function to set the translational and rotational axes.
302
        The translational axes are based on the principal axes of the unit
303
        one level larger than the level we are interested in (except for
304
        the polymer level where there is no larger unit). The rotational
305
        axes use the covalent links between residues or atoms where possible
306
        to define the axes, or if the unit is not bonded to others of the
307
        same level the prinicpal axes of the unit are used.
308

309
        Input
310
        -----
311
        data_container : the information about the molecule and trajectory
312
        level : the level (united atom, residue, or polymer) of interest
313
        index : residue index (integer)
314

315
        Output
316
        ------
317
        trans_axes : translational axes
318
        rot_axes : rotational axes
319
        """
NEW
320
        index = int(index)
×
321

NEW
322
        if level == "polymer":
×
323
            # for polymer use principle axis for both translation and rotation
NEW
324
            trans_axes = data_container.atoms.principal_axes()
×
NEW
325
            rot_axes = data_container.atoms.principal_axes()
×
326

NEW
327
        if level == "residue":
×
328
            # Translation
329
            # for residues use principal axes of whole molecule for translation
NEW
330
            trans_axes = data_container.atoms.principal_axes()
×
331

332
            # Rotation
333
            # find bonds between atoms in residue of interest and other residues
334
            # we are assuming bonds only exist between adjacent residues
335
            # (linear chains of residues)
336
            # TODO refine selection so that it will work for branched polymers
NEW
337
            index_prev = index - 1
×
NEW
338
            index_next = index + 1
×
NEW
339
            atom_set = data_container.select_atoms(
×
340
                f"(resindex {index_prev} or resindex {index_next}) "
341
                f"and bonded resid {index}"
342
            )
NEW
343
            residue = data_container.select_atoms(f"resindex {index}")
×
344

NEW
345
            if len(atom_set) == 0:
×
346
                # if no bonds to other residues use pricipal axes of residue
NEW
347
                rot_axes = residue.atoms.principal_axes()
×
348

349
            else:
350
                # set center of rotation to center of mass of the residue
NEW
351
                center = residue.atoms.center_of_mass()
×
352

353
                # get vector for average position of bonded atoms
NEW
354
                vector = self.get_avg_pos(atom_set, center)
×
355

356
                # use spherical coordinates function to get rotational axes
NEW
357
                rot_axes = self.get_sphCoord_axes(vector)
×
358

NEW
359
        if level == "united_atom":
×
360
            # Translation
361
            # for united atoms use principal axes of residue for translation
NEW
362
            trans_axes = data_container.residues.principal_axes()
×
363

364
            # Rotation
365
            # for united atoms use heavy atoms bonded to the heavy atom
NEW
366
            atom_set = data_container.select_atoms(
×
367
                f"not name H* and bonded index {index}"
368
            )
369

370
            # center at position of heavy atom
NEW
371
            atom_group = data_container.select_atoms(f"index {index}")
×
NEW
372
            center = atom_group.positions[0]
×
373

374
            # get vector for average position of hydrogens
NEW
375
            vector = self.get_avg_pos(atom_set, center)
×
376

377
            # use spherical coordinates function to get rotational axes
NEW
378
            rot_axes = self.get_sphCoord_axes(vector)
×
379

NEW
380
        logger.debug(f"Translational Axes: {trans_axes}")
×
NEW
381
        logger.debug(f"Rotational Axes: {rot_axes}")
×
382

NEW
383
        return trans_axes, rot_axes
×
384

385
    def get_avg_pos(self, atom_set, center):
3✔
386
        """
387
        Function to get the average position of a set of atoms.
388

389
        Input
390
        -----
391
        atoms : MDAnalysis atom group
392
        center : position for center of rotation
393

394
        Output
395
        ------
396
        avg_position : three dimensional vector
397
        """
398
        # start with an empty vector
NEW
399
        avg_position = np.zeros((3))
×
400

401
        # get number of atoms
NEW
402
        number_atoms = len(atom_set.names)
×
403

NEW
404
        if number_atoms != 0:
×
405
            # sum positions for all atoms in the given set
NEW
406
            for atom_index in range(number_atoms):
×
NEW
407
                atom_position = atom_set.atoms[atom_index].position
×
408

NEW
409
                avg_position += atom_position
×
410

NEW
411
            avg_position /= number_atoms  # divide by number of atoms to get average
×
412

413
        else:
414
            # if no atoms in set the unit has no bonds to restrict its rotational
415
            # motion, so we can use a random vector to get the spherical
416
            # coordinates axes
NEW
417
            avg_position = np.random.random(3)
×
418

419
        # transform the average position to a coordinate system with the origin
420
        # at center
NEW
421
        avg_position = avg_position - center
×
422

NEW
423
        logger.debug(f"Average Position: {avg_position}")
×
424

NEW
425
        return avg_position
×
426

427
    def get_sphCoord_axes(self, arg_r):
3✔
428
        """
429
        For a given vector in space, treat it is a radial vector rooted at
430
        0,0,0 and derive a curvilinear coordinate system according to the
431
        rules of polar spherical coordinates
432
        """
433

NEW
434
        x2y2 = arg_r[0] ** 2 + arg_r[1] ** 2
×
NEW
435
        r2 = x2y2 + arg_r[2] ** 2
×
436

437
        # Check for division by zero
NEW
438
        if r2 == 0.0:
×
NEW
439
            raise ValueError("r2 is zero, cannot compute spherical coordinates.")
×
440

NEW
441
        if x2y2 == 0.0:
×
NEW
442
            raise ValueError("x2y2 is zero, cannot compute sin_phi and cos_phi.")
×
443

444
        # Check for non-negative values inside the square root
NEW
445
        if x2y2 / r2 < 0:
×
NEW
446
            raise ValueError(
×
447
                f"Negative value encountered for sin_theta calculation: {x2y2 / r2}. "
448
                f"Cannot take square root."
449
            )
450

NEW
451
        if x2y2 < 0:
×
NEW
452
            raise ValueError(
×
453
                f"Negative value encountered for sin_phi and cos_phi "
454
                f"calculation: {x2y2}. "
455
                f"Cannot take square root."
456
            )
457

NEW
458
        if x2y2 != 0.0:
×
NEW
459
            sin_theta = np.sqrt(x2y2 / r2)
×
NEW
460
            cos_theta = arg_r[2] / np.sqrt(r2)
×
461

NEW
462
            sin_phi = arg_r[1] / np.sqrt(x2y2)
×
NEW
463
            cos_phi = arg_r[0] / np.sqrt(x2y2)
×
464

465
        else:
NEW
466
            sin_theta = 0.0
×
NEW
467
            cos_theta = 1
×
468

NEW
469
            sin_phi = 0.0
×
NEW
470
            cos_phi = 1
×
471

472
        # if abs(sin_theta) > 1 or abs(sin_phi) > 1:
473
        #     print('Bad sine : T {} , P {}'.format(sin_theta, sin_phi))
474

475
        # cos_theta = np.sqrt(1 - sin_theta*sin_theta)
476
        # cos_phi = np.sqrt(1 - sin_phi*sin_phi)
477

478
        # print('{} {} {}'.format(*arg_r))
479
        # print('Sin T : {}, cos T : {}'.format(sin_theta, cos_theta))
480
        # print('Sin P : {}, cos P : {}'.format(sin_phi, cos_phi))
481

NEW
482
        spherical_basis = np.zeros((3, 3))
×
483

484
        # r^
NEW
485
        spherical_basis[0, :] = np.asarray(
×
486
            [sin_theta * cos_phi, sin_theta * sin_phi, cos_theta]
487
        )
488

489
        # Theta^
NEW
490
        spherical_basis[1, :] = np.asarray(
×
491
            [cos_theta * cos_phi, cos_theta * sin_phi, -sin_theta]
492
        )
493

494
        # Phi^
NEW
495
        spherical_basis[2, :] = np.asarray([-sin_phi, cos_phi, 0.0])
×
496

NEW
497
        logger.debug(f"Spherical Basis: {spherical_basis}")
×
498

NEW
499
        return spherical_basis
×
500

501
    def get_weighted_forces(
3✔
502
        self, data_container, bead, trans_axes, highest_level, force_partitioning=0.5
503
    ):
504
        """
505
        Function to calculate the mass weighted forces for a given bead.
506

507
        Input
508
        -----
509
        bead : the part of the system to be considered
510
        trans_axes : the axes relative to which the forces are located
511

512
        Output
513
        ------
514
        weighted_force : the mass weighted sum of the forces in the bead
515
        """
516

NEW
517
        forces_trans = np.zeros((3,))
×
518

519
        # Sum forces from all atoms in the bead
NEW
520
        for atom in bead.atoms:
×
521
            # update local forces in translational axes
NEW
522
            forces_local = np.matmul(trans_axes, data_container.atoms[atom.index].force)
×
NEW
523
            forces_trans += forces_local
×
524

NEW
525
        if highest_level:
×
526
            # multiply by the force_partitioning parameter to avoid double counting
527
            # of the forces on weakly correlated atoms
528
            # the default value of force_partitioning is 0.5 (dividing by two)
NEW
529
            forces_trans = force_partitioning * forces_trans
×
530

531
        # divide the sum of forces by the mass of the bead to get the weighted forces
NEW
532
        mass = bead.total_mass()
×
533

534
        # Check that mass is positive to avoid division by 0 or negative values inside
535
        # sqrt
NEW
536
        if mass <= 0:
×
NEW
537
            raise ValueError(
×
538
                f"Invalid mass value: {mass}. Mass must be positive to compute the "
539
                f"square root."
540
            )
541

NEW
542
        weighted_force = forces_trans / np.sqrt(mass)
×
543

NEW
544
        logger.debug(f"Weighted Force: {weighted_force}")
×
545

NEW
546
        return weighted_force
×
547

548
    def get_weighted_torques(
3✔
549
        self, data_container, bead, rot_axes, force_partitioning=0.5
550
    ):
551
        """
552
        Function to calculate the moment of inertia weighted torques for a given bead.
553

554
        This function computes torques in a rotated frame and then weights them using
555
        the moment of inertia tensor. To prevent numerical instability, it treats
556
        extremely small diagonal elements of the moment of inertia tensor as zero
557
        (since values below machine precision are effectively zero). This avoids
558
        unnecessary use of extended precision (e.g., float128).
559

560
        Additionally, if the computed torque is already zero, the function skips
561
        the division step, reducing unnecessary computations and potential errors.
562

563
        Parameters
564
        ----------
565
        data_container : object
566
            Contains atomic positions and forces.
567
        bead : object
568
            The part of the molecule to be considered.
569
        rot_axes : np.ndarray
570
            The axes relative to which the forces and coordinates are located.
571
        force_partitioning : float, optional
572
            Factor to adjust force contributions, default is 0.5.
573

574
        Returns
575
        -------
576
        np.ndarray
577
            The mass-weighted sum of the torques in the bead.
578
        """
579

NEW
580
        torques = np.zeros((3,))
×
NEW
581
        weighted_torque = np.zeros((3,))
×
582

NEW
583
        for atom in bead.atoms:
×
584

585
            # update local coordinates in rotational axes
NEW
586
            coords_rot = (
×
587
                data_container.atoms[atom.index].position - bead.center_of_mass()
588
            )
NEW
589
            coords_rot = np.matmul(rot_axes, coords_rot)
×
590
            # update local forces in rotational frame
NEW
591
            forces_rot = np.matmul(rot_axes, data_container.atoms[atom.index].force)
×
592

593
            # multiply by the force_partitioning parameter to avoid double counting
594
            # of the forces on weakly correlated atoms
595
            # the default value of force_partitioning is 0.5 (dividing by two)
NEW
596
            forces_rot = force_partitioning * forces_rot
×
597

598
            # define torques (cross product of coordinates and forces) in rotational
599
            # axes
NEW
600
            torques_local = np.cross(coords_rot, forces_rot)
×
NEW
601
            torques += torques_local
×
602

603
        # divide by moment of inertia to get weighted torques
604
        # moment of inertia is a 3x3 tensor
605
        # the weighting is done in each dimension (x,y,z) using the diagonal
606
        # elements of the moment of inertia tensor
NEW
607
        moment_of_inertia = bead.moment_of_inertia()
×
608

NEW
609
        for dimension in range(3):
×
610
            # Skip calculation if torque is already zero
NEW
611
            if np.isclose(torques[dimension], 0):
×
NEW
612
                weighted_torque[dimension] = 0
×
NEW
613
                continue
×
614

615
            # Check for zero moment of inertia
NEW
616
            if np.isclose(moment_of_inertia[dimension, dimension], 0):
×
NEW
617
                raise ZeroDivisionError(
×
618
                    f"Attempted to divide by zero moment of inertia in dimension "
619
                    f"{dimension}."
620
                )
621

622
            # Check for negative moment of inertia
NEW
623
            if moment_of_inertia[dimension, dimension] < 0:
×
NEW
624
                raise ValueError(
×
625
                    f"Negative value encountered for moment of inertia: "
626
                    f"{moment_of_inertia[dimension, dimension]} "
627
                    f"Cannot compute weighted torque."
628
                )
629

630
            # Compute weighted torque
NEW
631
            weighted_torque[dimension] = torques[dimension] / np.sqrt(
×
632
                moment_of_inertia[dimension, dimension]
633
            )
634

NEW
635
        logger.debug(f"Weighted Torque: {weighted_torque}")
×
636

NEW
637
        return weighted_torque
×
638

639
    def create_submatrix(self, data_i, data_j, number_frames):
3✔
640
        """
641
        Function for making covariance matrices.
642

643
        Input
644
        -----
645
        data_i : values for bead i
646
        data_j : valuees for bead j
647

648
        Output
649
        ------
650
        submatrix : 3x3 matrix for the covariance between i and j
651
        """
652

653
        # Start with 3 by 3 matrix of zeros
NEW
654
        submatrix = np.zeros((3, 3))
×
655

656
        # For each frame calculate the outer product (cross product) of the data from
657
        # the two beads and add the result to the submatrix
NEW
658
        for frame in range(number_frames):
×
NEW
659
            outer_product_matrix = np.outer(data_i[frame], data_j[frame])
×
NEW
660
            submatrix = np.add(submatrix, outer_product_matrix)
×
661

662
        # Divide by the number of frames to get the average
NEW
663
        submatrix /= number_frames
×
664

NEW
665
        logger.debug(f"Submatrix: {submatrix}")
×
666

NEW
667
        return submatrix
×
668

669
    def filter_zero_rows_columns(self, arg_matrix):
3✔
670
        """
671
        function for removing rows and columns that contain only zeros from a matrix
672

673
        Input
674
        -----
675
        arg_matrix : matrix
676

677
        Output
678
        ------
679
        arg_matrix : the reduced size matrix
680
        """
681

682
        # record the initial size
NEW
683
        init_shape = np.shape(arg_matrix)
×
684

NEW
685
        zero_indices = list(
×
686
            filter(
687
                lambda row: np.all(np.isclose(arg_matrix[row, :], 0.0)),
688
                np.arange(np.shape(arg_matrix)[0]),
689
            )
690
        )
NEW
691
        all_indices = np.ones((np.shape(arg_matrix)[0]), dtype=bool)
×
NEW
692
        all_indices[zero_indices] = False
×
NEW
693
        arg_matrix = arg_matrix[all_indices, :]
×
694

NEW
695
        all_indices = np.ones((np.shape(arg_matrix)[1]), dtype=bool)
×
NEW
696
        zero_indices = list(
×
697
            filter(
698
                lambda col: np.all(np.isclose(arg_matrix[:, col], 0.0)),
699
                np.arange(np.shape(arg_matrix)[1]),
700
            )
701
        )
NEW
702
        all_indices[zero_indices] = False
×
NEW
703
        arg_matrix = arg_matrix[:, all_indices]
×
704

705
        # get the final shape
NEW
706
        final_shape = np.shape(arg_matrix)
×
707

NEW
708
        if init_shape != final_shape:
×
NEW
709
            logger.debug(
×
710
                "A shape change has occurred ({},{}) -> ({}, {})".format(
711
                    *init_shape, *final_shape
712
                )
713
            )
714

NEW
715
        logger.debug(f"arg_matrix: {arg_matrix}")
×
716

NEW
717
        return arg_matrix
×
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

© 2026 Coveralls, Inc