Skip to content

Models

Boundary

fiber_matrix.models.boundary

BoundaryNode

Node info for the boundary of fibers or the rve.

Source code in fiber_matrix/models/boundary.py
243
244
245
246
247
248
249
250
251
252
253
254
255
class BoundaryNode:
    """Node info for the boundary of fibers or the rve."""

    def __init__(
        self,
        point: np.ndarray,
        lies_on_rve_boundary=False,
        boundary: Optional[LinearBoundary] = None,
    ):
        self.point = np.array(point)
        self.index = -1
        self.lies_on_rve_boundary = lies_on_rve_boundary
        self.boundary = boundary

BoundaryType

Bases: Enum

Enumeration class for denoting a type of boundary.

Attributes:

Name Type Description
PERIODIC int

Fibers can exist across boundary pair to be periodic.

FINITE int

Fibers cannot cross the boundary.

SYMMETRIC int

Fibers are only allowed to lie exactly half way or not at all.

Source code in fiber_matrix/models/boundary.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BoundaryType(Enum):
    """Enumeration class for denoting a type of boundary.

    Attributes
    ----------
    PERIODIC : int
        Fibers can exist across boundary pair to be periodic.
    FINITE : int
        Fibers cannot cross the boundary.
    SYMMETRIC : int
        Fibers are only allowed to lie exactly half way or not at all.
    """

    FINITE = 0
    SYMMETRIC = 1
    PERIODIC = 2

    def get_color(self) -> Tuple[float, float, float, float]:
        """Returns the color associated with the boundary type.

        Returns
        -------
        Tuple[float, float, float, float]
            RGBA color tuple.
        """
        if self.name == "PERIODIC":
            return (1, 0, 0, 1)
        if self.name == "FINITE":
            return (0, 0, 0, 1)
        if self.name == "SYMMETRIC":
            return (0, 0.75, 0, 1)

get_color()

Returns the color associated with the boundary type.

Returns:

Type Description
Tuple[float, float, float, float]

RGBA color tuple.

Source code in fiber_matrix/models/boundary.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def get_color(self) -> Tuple[float, float, float, float]:
    """Returns the color associated with the boundary type.

    Returns
    -------
    Tuple[float, float, float, float]
        RGBA color tuple.
    """
    if self.name == "PERIODIC":
        return (1, 0, 0, 1)
    if self.name == "FINITE":
        return (0, 0, 0, 1)
    if self.name == "SYMMETRIC":
        return (0, 0.75, 0, 1)

LinearBoundary

A boundary of the RVE that is defined by a line segment.

Parameters:

Name Type Description Default
points List[ndarray]

List of 2 points defining the start and end of the boundary segment.

required
point_indices List[int]

Indices of the points in the global boundary point list.

required
btype BoundaryType

The type of the boundary (PERIODIC, FINITE, SYMMETRIC).

required
Source code in fiber_matrix/models/boundary.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
class LinearBoundary:
    """A boundary of the RVE that is defined by a line segment.

    Parameters
    ----------
    points : List[np.ndarray]
        List of 2 points defining the start and end of the boundary segment.
    point_indices : List[int]
        Indices of the points in the global boundary point list.
    btype : BoundaryType
        The type of the boundary (PERIODIC, FINITE, SYMMETRIC).
    """

    def __init__(
        self, points: List[np.ndarray], point_indices: List[int], btype: BoundaryType
    ):
        self.points = [np.array(point) for point in points]
        self.point_indices = point_indices
        self.pair: Optional["LinearBoundary"] = None
        self.type = btype
        self.fiber_intersection_nodes = []
        self.index = 0

    def get_length(self) -> float:
        """Calculates the length of the boundary segment.

        Returns
        -------
        float
            The length of the boundary.
        """
        return np.linalg.norm(self.points[1] - self.points[0])

    def is_pair(self, other_boundary: "LinearBoundary") -> bool:
        """Checks if this boundary forms a periodic pair with another boundary.

        Parameters
        ----------
        other_boundary : LinearBoundary
            The other boundary to check against.

        Returns
        -------
        bool
            True if the boundaries are geometric pairs (same length, parallel, opposite direction), False otherwise.
        """
        if self.get_length() != other_boundary.get_length():
            return False
        vec1 = self.points[0] - other_boundary.points[1]
        vec2 = self.points[1] - other_boundary.points[0]
        if np.linalg.norm(vec2 - vec1) > 1e-8:
            return False
        return True

    def get_periodic_vector(self) -> Optional[np.ndarray]:
        """Gets the translation vector to the paired boundary.

        Returns
        -------
        Optional[np.ndarray]
            The translation vector if a pair exists, None otherwise.
        """
        if self.pair is not None:
            return self.pair.points[1] - self.points[0]
        return None

    def get_move_vector(self) -> np.ndarray:
        """Get the vector that should be used to move a fiber if it intersects this boundary.

        Returns
        -------
        np.ndarray
            A normalized 2D vector pointing strictly inward from the boundary.
        """
        segment_vector = self.points[1] - self.points[0]
        segment_vector = np.append(segment_vector, 0.0)
        segment_vector = segment_vector / np.linalg.norm(segment_vector)
        z_vector = np.array([0.0, 0.0, 1.0])
        return np.cross(z_vector, segment_vector)[0:2]

    def get_point_relative_position(self, point: np.ndarray) -> float:
        """Determines the position of a point relative to the boundary line.

        Parameters
        ----------
        point : np.ndarray
            The 2D point to check.

        Returns
        -------
        float
            Positive value if the point lies inside (to the left of the boundary vector),
            negative if outside.
        """
        segment_vector = self.points[1] - self.points[0]
        segment_vector_mag = np.linalg.norm(segment_vector)
        unit_segment_vector = segment_vector / segment_vector_mag
        point_to_segment_start = point - self.points[0]
        unit_point_to_segment_start = point_to_segment_start / np.linalg.norm(
            point_to_segment_start
        )
        cross_product = np.cross(unit_segment_vector, unit_point_to_segment_start)
        return cross_product

    def _closest_point_to_fiber(
        self, fiber_center: np.ndarray, within_boundary_segment=True
    ) -> np.ndarray:
        """Calculates the closest point on the boundary segment to a fiber center.

        Parameters
        ----------
        fiber_center : np.ndarray
            The center of the fiber.
        within_boundary_segment : bool, optional
            If True, clamps the closest point to lie within the segment endpoints.
            Default is True.

        Returns
        -------
        np.ndarray
            The coordinates of the closest point.
        """
        segment_vector = self.points[1] - self.points[0]
        segment_vector_mag = np.linalg.norm(segment_vector)
        circle_to_point_a = fiber_center - self.points[0]
        unit_segment_vector = segment_vector / segment_vector_mag
        projection = np.dot(circle_to_point_a, unit_segment_vector)
        if projection <= 0 and within_boundary_segment:
            return self.points[0]
        if projection >= segment_vector_mag and within_boundary_segment:
            return self.points[1]
        projection_vector = unit_segment_vector * projection
        return projection_vector + self.points[0]

    def check_collision(
        self, fiber_center: np.ndarray, fiber_radius: float, eps=0.0
    ) -> bool:
        """Checks if a fiber collides with the boundary.

        Parameters
        ----------
        fiber_center : np.ndarray
            The center of the fiber.
        fiber_radius : float
            The radius of the fiber.
        eps : float, optional
            A small epsilon buffer ratio. Default is 0.0.

        Returns
        -------
        bool
            True if the distance from the fiber center to the boundary is less than the buffered radius.
        """
        closest_point = self._closest_point_to_fiber(fiber_center)
        fiber_to_circle_shortest_vector = fiber_center - closest_point
        fiber_to_circle_shortest_vector_mag = np.linalg.norm(
            fiber_to_circle_shortest_vector
        )
        if fiber_to_circle_shortest_vector_mag <= (1.0 + eps) * fiber_radius:
            return True
        return False

    def get_distance_to_fiber(self, fiber_center: np.ndarray) -> float:
        """Calculates the Euclidean distance from the fiber center to the boundary.

        Parameters
        ----------
        fiber_center : np.ndarray
            The center of the fiber.

        Returns
        -------
        float
            Distance to the boundary.
        """
        closest_point = self._closest_point_to_fiber(fiber_center)
        fiber_to_circle_shortest_vector = fiber_center - closest_point
        return np.linalg.norm(fiber_to_circle_shortest_vector)

    def get_intersection_points(
        self, fiber_center: np.ndarray, fiber_radius: float
    ) -> List[np.ndarray]:
        closest_point = self._closest_point_to_fiber(
            fiber_center, within_boundary_segment=False
        )
        segment_vector = self.points[1] - self.points[0]
        segment_vector_mag = np.linalg.norm(segment_vector)
        unit_segment_vector = segment_vector / segment_vector_mag
        closest_to_center = fiber_center - closest_point
        c = np.linalg.norm(closest_to_center)
        if c == fiber_radius:
            return [closest_point]
        else:
            ds = np.sqrt(fiber_radius**2 - c**2)
            i1 = closest_point + ds * unit_segment_vector
            i2 = closest_point - ds * unit_segment_vector
            intersections = []
            for p in [i1, i2]:
                dot_product = np.dot(p - self.points[0], segment_vector)
                if dot_product >= 0 and dot_product <= segment_vector_mag**2:
                    intersections.append(p)
            return intersections

check_collision(fiber_center, fiber_radius, eps=0.0)

Checks if a fiber collides with the boundary.

Parameters:

Name Type Description Default
fiber_center ndarray

The center of the fiber.

required
fiber_radius float

The radius of the fiber.

required
eps float

A small epsilon buffer ratio. Default is 0.0.

0.0

Returns:

Type Description
bool

True if the distance from the fiber center to the boundary is less than the buffered radius.

Source code in fiber_matrix/models/boundary.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def check_collision(
    self, fiber_center: np.ndarray, fiber_radius: float, eps=0.0
) -> bool:
    """Checks if a fiber collides with the boundary.

    Parameters
    ----------
    fiber_center : np.ndarray
        The center of the fiber.
    fiber_radius : float
        The radius of the fiber.
    eps : float, optional
        A small epsilon buffer ratio. Default is 0.0.

    Returns
    -------
    bool
        True if the distance from the fiber center to the boundary is less than the buffered radius.
    """
    closest_point = self._closest_point_to_fiber(fiber_center)
    fiber_to_circle_shortest_vector = fiber_center - closest_point
    fiber_to_circle_shortest_vector_mag = np.linalg.norm(
        fiber_to_circle_shortest_vector
    )
    if fiber_to_circle_shortest_vector_mag <= (1.0 + eps) * fiber_radius:
        return True
    return False

get_distance_to_fiber(fiber_center)

Calculates the Euclidean distance from the fiber center to the boundary.

Parameters:

Name Type Description Default
fiber_center ndarray

The center of the fiber.

required

Returns:

Type Description
float

Distance to the boundary.

Source code in fiber_matrix/models/boundary.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def get_distance_to_fiber(self, fiber_center: np.ndarray) -> float:
    """Calculates the Euclidean distance from the fiber center to the boundary.

    Parameters
    ----------
    fiber_center : np.ndarray
        The center of the fiber.

    Returns
    -------
    float
        Distance to the boundary.
    """
    closest_point = self._closest_point_to_fiber(fiber_center)
    fiber_to_circle_shortest_vector = fiber_center - closest_point
    return np.linalg.norm(fiber_to_circle_shortest_vector)

get_length()

Calculates the length of the boundary segment.

Returns:

Type Description
float

The length of the boundary.

Source code in fiber_matrix/models/boundary.py
62
63
64
65
66
67
68
69
70
def get_length(self) -> float:
    """Calculates the length of the boundary segment.

    Returns
    -------
    float
        The length of the boundary.
    """
    return np.linalg.norm(self.points[1] - self.points[0])

get_move_vector()

Get the vector that should be used to move a fiber if it intersects this boundary.

Returns:

Type Description
ndarray

A normalized 2D vector pointing strictly inward from the boundary.

Source code in fiber_matrix/models/boundary.py
105
106
107
108
109
110
111
112
113
114
115
116
117
def get_move_vector(self) -> np.ndarray:
    """Get the vector that should be used to move a fiber if it intersects this boundary.

    Returns
    -------
    np.ndarray
        A normalized 2D vector pointing strictly inward from the boundary.
    """
    segment_vector = self.points[1] - self.points[0]
    segment_vector = np.append(segment_vector, 0.0)
    segment_vector = segment_vector / np.linalg.norm(segment_vector)
    z_vector = np.array([0.0, 0.0, 1.0])
    return np.cross(z_vector, segment_vector)[0:2]

get_periodic_vector()

Gets the translation vector to the paired boundary.

Returns:

Type Description
Optional[ndarray]

The translation vector if a pair exists, None otherwise.

Source code in fiber_matrix/models/boundary.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def get_periodic_vector(self) -> Optional[np.ndarray]:
    """Gets the translation vector to the paired boundary.

    Returns
    -------
    Optional[np.ndarray]
        The translation vector if a pair exists, None otherwise.
    """
    if self.pair is not None:
        return self.pair.points[1] - self.points[0]
    return None

get_point_relative_position(point)

Determines the position of a point relative to the boundary line.

Parameters:

Name Type Description Default
point ndarray

The 2D point to check.

required

Returns:

Type Description
float

Positive value if the point lies inside (to the left of the boundary vector), negative if outside.

Source code in fiber_matrix/models/boundary.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def get_point_relative_position(self, point: np.ndarray) -> float:
    """Determines the position of a point relative to the boundary line.

    Parameters
    ----------
    point : np.ndarray
        The 2D point to check.

    Returns
    -------
    float
        Positive value if the point lies inside (to the left of the boundary vector),
        negative if outside.
    """
    segment_vector = self.points[1] - self.points[0]
    segment_vector_mag = np.linalg.norm(segment_vector)
    unit_segment_vector = segment_vector / segment_vector_mag
    point_to_segment_start = point - self.points[0]
    unit_point_to_segment_start = point_to_segment_start / np.linalg.norm(
        point_to_segment_start
    )
    cross_product = np.cross(unit_segment_vector, unit_point_to_segment_start)
    return cross_product

is_pair(other_boundary)

Checks if this boundary forms a periodic pair with another boundary.

Parameters:

Name Type Description Default
other_boundary LinearBoundary

The other boundary to check against.

required

Returns:

Type Description
bool

True if the boundaries are geometric pairs (same length, parallel, opposite direction), False otherwise.

Source code in fiber_matrix/models/boundary.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def is_pair(self, other_boundary: "LinearBoundary") -> bool:
    """Checks if this boundary forms a periodic pair with another boundary.

    Parameters
    ----------
    other_boundary : LinearBoundary
        The other boundary to check against.

    Returns
    -------
    bool
        True if the boundaries are geometric pairs (same length, parallel, opposite direction), False otherwise.
    """
    if self.get_length() != other_boundary.get_length():
        return False
    vec1 = self.points[0] - other_boundary.points[1]
    vec2 = self.points[1] - other_boundary.points[0]
    if np.linalg.norm(vec2 - vec1) > 1e-8:
        return False
    return True

Fiber

fiber_matrix.models.fiber

Fiber

The basic fiber class representing a circular inclusion.

Parameters:

Name Type Description Default
center ndarray

The coordinates of the fiber center.

required
radius float

The radius of the fiber.

required
Source code in fiber_matrix/models/fiber.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
class Fiber:
    """The basic fiber class representing a circular inclusion.

    Parameters
    ----------
    center : np.ndarray
        The coordinates of the fiber center.
    radius : float
        The radius of the fiber.
    """

    def __init__(self, center: np.ndarray, radius: float):
        self.center = np.array(center, dtype=float)
        self.radius = radius
        self.boundary_nodes: List[BoundaryNode] = []
        self.coord_inside_rve: Optional[List[float]] = None
        self.neighbors: Set["Fiber"] = set()
        self.vec_moved_since_neighbor_update = np.array([0.0, 0.0])

    def __str__(self):
        return f"Fiber Center: {self.center}\tr={self.radius}"

    def get_vector_to_other(self, other_fiber: "Fiber") -> np.ndarray:
        """Calculates the vector pointing from this fiber to another fiber.

        Parameters
        ----------
        other_fiber : Fiber
            The target fiber.

        Returns
        -------
        np.ndarray
            Vector from self to other.
        """
        return np.array(other_fiber.center) - np.array(self.center)

    def move(self, move_vec: np.ndarray):
        """Translates the fiber by the given vector.

        Parameters
        ----------
        move_vec : np.ndarray
            Translation vector.
        """
        self.center = self.center + np.array(move_vec)

    def fix_overlap_with_neighbors(
        self, boundaries: List[LinearBoundary], min_space_between_fibers: float
    ) -> bool:
        """Iteratively resolves overlaps with neighboring fibers.

        Parameters
        ----------
        boundaries : List[LinearBoundary]
            List of RVE boundaries for constraint checking.
        min_space_between_fibers : float
            Minimum allowed distance between fiber surfaces.

        Returns
        -------
        bool
            True if any overlap was found and corrected, False otherwise.
        """
        overlap_found = False
        # Sort neighbors for deterministic behavior
        sorted_neighbors = sorted(
            list(self.neighbors), key=lambda f: f.center[0]
        )  # Simple sort by x-coordinate
        for nf in sorted_neighbors:
            vec_to_other = self.get_vector_to_other(nf)
            dist = np.linalg.norm(vec_to_other)
            sum_radii = self.radius + nf.radius
            if dist < sum_radii + min_space_between_fibers:
                overlap_found = True
                unit_vec_to_other = vec_to_other / dist
                proportion_to_move_this_fiber = nf.radius / sum_radii
                proportion_to_move_other_fiber = 1.0 - proportion_to_move_this_fiber
                total_move_back = (
                    sum_radii + min_space_between_fibers - dist + sum_radii * 1e-3
                )

                this_nudge = (
                    -proportion_to_move_this_fiber * total_move_back * unit_vec_to_other
                )
                other_nudge = (
                    proportion_to_move_other_fiber * total_move_back * unit_vec_to_other
                )

                self.move(this_nudge)
                self.adjust_for_bounds(boundaries)
                nf.move(other_nudge)
                nf.adjust_for_bounds(boundaries)
        return overlap_found

    def adjust_for_bounds(self, boundaries: List[LinearBoundary], eps=5.0e-2):
        """Checks to make sure this fiber does not violate any specified boundaries
        and will move it if necessary.
        """
        for boundary in boundaries:
            if boundary.type == BoundaryType.FINITE and boundary.check_collision(
                self.center, self.radius, eps
            ):
                move_vector = boundary.get_move_vector()
                distance_to_center = boundary.get_distance_to_fiber(self.center)
                self.center = (
                    self.center
                    + abs(distance_to_center - (1.0 + eps) * self.radius) * move_vector
                )

            if boundary.type == BoundaryType.SYMMETRIC and boundary.check_collision(
                self.center, self.radius, eps
            ):
                move_vector = boundary.get_move_vector()
                distance_to_center = boundary.get_distance_to_fiber(self.center)
                if distance_to_center < self.radius / 2.0:
                    self.center = (
                        self.center
                        - abs(distance_to_center - (1.0 + eps) * self.radius)
                        * move_vector
                    )
                else:
                    self.center = (
                        self.center
                        + abs(distance_to_center - (1.0 + eps) * self.radius)
                        * move_vector
                    )

adjust_for_bounds(boundaries, eps=0.05)

Checks to make sure this fiber does not violate any specified boundaries and will move it if necessary.

Source code in fiber_matrix/models/fiber.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def adjust_for_bounds(self, boundaries: List[LinearBoundary], eps=5.0e-2):
    """Checks to make sure this fiber does not violate any specified boundaries
    and will move it if necessary.
    """
    for boundary in boundaries:
        if boundary.type == BoundaryType.FINITE and boundary.check_collision(
            self.center, self.radius, eps
        ):
            move_vector = boundary.get_move_vector()
            distance_to_center = boundary.get_distance_to_fiber(self.center)
            self.center = (
                self.center
                + abs(distance_to_center - (1.0 + eps) * self.radius) * move_vector
            )

        if boundary.type == BoundaryType.SYMMETRIC and boundary.check_collision(
            self.center, self.radius, eps
        ):
            move_vector = boundary.get_move_vector()
            distance_to_center = boundary.get_distance_to_fiber(self.center)
            if distance_to_center < self.radius / 2.0:
                self.center = (
                    self.center
                    - abs(distance_to_center - (1.0 + eps) * self.radius)
                    * move_vector
                )
            else:
                self.center = (
                    self.center
                    + abs(distance_to_center - (1.0 + eps) * self.radius)
                    * move_vector
                )

fix_overlap_with_neighbors(boundaries, min_space_between_fibers)

Iteratively resolves overlaps with neighboring fibers.

Parameters:

Name Type Description Default
boundaries List[LinearBoundary]

List of RVE boundaries for constraint checking.

required
min_space_between_fibers float

Minimum allowed distance between fiber surfaces.

required

Returns:

Type Description
bool

True if any overlap was found and corrected, False otherwise.

Source code in fiber_matrix/models/fiber.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def fix_overlap_with_neighbors(
    self, boundaries: List[LinearBoundary], min_space_between_fibers: float
) -> bool:
    """Iteratively resolves overlaps with neighboring fibers.

    Parameters
    ----------
    boundaries : List[LinearBoundary]
        List of RVE boundaries for constraint checking.
    min_space_between_fibers : float
        Minimum allowed distance between fiber surfaces.

    Returns
    -------
    bool
        True if any overlap was found and corrected, False otherwise.
    """
    overlap_found = False
    # Sort neighbors for deterministic behavior
    sorted_neighbors = sorted(
        list(self.neighbors), key=lambda f: f.center[0]
    )  # Simple sort by x-coordinate
    for nf in sorted_neighbors:
        vec_to_other = self.get_vector_to_other(nf)
        dist = np.linalg.norm(vec_to_other)
        sum_radii = self.radius + nf.radius
        if dist < sum_radii + min_space_between_fibers:
            overlap_found = True
            unit_vec_to_other = vec_to_other / dist
            proportion_to_move_this_fiber = nf.radius / sum_radii
            proportion_to_move_other_fiber = 1.0 - proportion_to_move_this_fiber
            total_move_back = (
                sum_radii + min_space_between_fibers - dist + sum_radii * 1e-3
            )

            this_nudge = (
                -proportion_to_move_this_fiber * total_move_back * unit_vec_to_other
            )
            other_nudge = (
                proportion_to_move_other_fiber * total_move_back * unit_vec_to_other
            )

            self.move(this_nudge)
            self.adjust_for_bounds(boundaries)
            nf.move(other_nudge)
            nf.adjust_for_bounds(boundaries)
    return overlap_found

get_vector_to_other(other_fiber)

Calculates the vector pointing from this fiber to another fiber.

Parameters:

Name Type Description Default
other_fiber Fiber

The target fiber.

required

Returns:

Type Description
ndarray

Vector from self to other.

Source code in fiber_matrix/models/fiber.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def get_vector_to_other(self, other_fiber: "Fiber") -> np.ndarray:
    """Calculates the vector pointing from this fiber to another fiber.

    Parameters
    ----------
    other_fiber : Fiber
        The target fiber.

    Returns
    -------
    np.ndarray
        Vector from self to other.
    """
    return np.array(other_fiber.center) - np.array(self.center)

move(move_vec)

Translates the fiber by the given vector.

Parameters:

Name Type Description Default
move_vec ndarray

Translation vector.

required
Source code in fiber_matrix/models/fiber.py
45
46
47
48
49
50
51
52
53
def move(self, move_vec: np.ndarray):
    """Translates the fiber by the given vector.

    Parameters
    ----------
    move_vec : np.ndarray
        Translation vector.
    """
    self.center = self.center + np.array(move_vec)

PeriodicPrimaryFiber

Bases: Fiber

A class for storing a fiber that is in the RVE domain.

Source code in fiber_matrix/models/fiber.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
class PeriodicPrimaryFiber(Fiber):
    """A class for storing a fiber that is in the RVE domain."""

    def __init__(
        self,
        center: np.ndarray,
        radius: float,
        index: int,
        boundaries: Optional[List[LinearBoundary]] = None,
        ignore_ghost_fibers=False,
    ):
        super().__init__(center, radius)
        self.index = index
        self.ghost_fibers: List["PeriodicGhostFiber"] = []
        self.ignore_ghost_fibers = ignore_ghost_fibers
        if boundaries is not None:
            self.calc_ghost_fibers(boundaries)

    def calc_ghost_fibers(self, boundaries: List[LinearBoundary]):
        self.ghost_fibers = []
        if self.ignore_ghost_fibers:
            return

        ghost_centers = []
        for boundary in boundaries:
            if boundary.type == BoundaryType.PERIODIC:
                # Need to use the boundary check logic here
                # accessing helper method on boundary to get distance
                if boundary.get_distance_to_fiber(self.center) < 3.0 * self.radius:
                    ghost_center = self.center + boundary.get_periodic_vector()
                    ghost_centers.append(ghost_center)

        # Check intersection with multiple periodic boundaries (corner cases)
        intersected_periodic = []
        for boundary in boundaries:
            if boundary.type == BoundaryType.PERIODIC and boundary.check_collision(
                self.center, self.radius
            ):
                intersected_periodic.append(boundary)

        if len(intersected_periodic) == 2:
            ghost_centers.append(
                intersected_periodic[0].get_periodic_vector()
                + intersected_periodic[1].get_periodic_vector()
                + self.center
            )

        for ghost_center in ghost_centers:
            self.ghost_fibers.append(
                PeriodicGhostFiber(ghost_center, self.radius, self)
            )

    def adjust_for_bounds(self, boundaries: List[LinearBoundary], eps=5.0e-2):
        super().adjust_for_bounds(boundaries, eps)

    def move(self, move_vec: np.ndarray):
        super().move(move_vec)
        self.vec_moved_since_neighbor_update += np.array(move_vec)
        for ghost in self.ghost_fibers:
            ghost.move_actual(move_vec)

    def get_distance_since_last_neighbor_update(self) -> float:
        return np.linalg.norm(self.vec_moved_since_neighbor_update)

    def get_all_copies(self) -> List[Fiber]:
        fibers = list(self.ghost_fibers)
        fibers.append(self)
        return fibers

    def update_neighbors(
        self,
        fiber_center_kd_tree,
        fibers_with_ghosts: List[Fiber],
        fiber_diams_to_search: float,
    ):
        self.neighbors = set()
        self.vec_moved_since_neighbor_update = np.array([0.0, 0.0])

        # KDTree query returns distances and indices
        dists, indices = fiber_center_kd_tree.query(
            self.center,
            k=100,
            p=2,
            distance_upper_bound=self.radius * 2.0 * fiber_diams_to_search,
        )

        # Filter indices
        valid_indices = [i for i in indices if i < len(fibers_with_ghosts)]

        for i in valid_indices:
            other = fibers_with_ghosts[i]
            # Check logic from original: neighbors should have higher index to avoid double checking pairs
            if isinstance(other, PeriodicPrimaryFiber):
                if other.index > self.index:
                    self.neighbors.add(other)
            elif isinstance(other, PeriodicGhostFiber):
                if other.primary_fiber.index > self.index:
                    self.neighbors.add(other)

        for gf in self.ghost_fibers:
            gf.update_neighbors(
                fiber_center_kd_tree, fibers_with_ghosts, fiber_diams_to_search
            )

    def fix_overlap_with_neighbors(
        self, boundaries: List[LinearBoundary], min_space_between_fibers: float
    ) -> bool:
        found_overlap = super().fix_overlap_with_neighbors(
            boundaries, min_space_between_fibers
        )
        for ghost_fib in self.ghost_fibers:
            found_overlap |= ghost_fib.fix_overlap_with_neighbors(
                boundaries, min_space_between_fibers
            )
        return found_overlap