Skip to content

Generation

fiber_matrix.generation.placement

FiberPlacementSolver

Class enabling the random placement and subsequent overlap resolution of fibers.

Source code in fiber_matrix/generation/placement.py
 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
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
class FiberPlacementSolver:
    """Class enabling the random placement and subsequent overlap resolution of fibers."""

    def __init__(self):
        pass

    def solve_fiber_locations(
        self,
        fibers: List[PeriodicPrimaryFiber],
        boundaries: List[LinearBoundary],
        min_spacing_ratio: float,
        iterations_max: int,
        iteration_callback: Optional[
            Callable[[int, List[PeriodicPrimaryFiber], List[LinearBoundary]], None]
        ] = None,
    ) -> int:
        """
        Iteratively resolves overlaps between fibers to ensure minimum spacing.

        Parameters
        ----------
        fibers : List[PeriodicPrimaryFiber]
            List of fibers to overlap resolve.
        boundaries : List[LinearBoundary]
            List of RVE boundaries for constraints.
        min_spacing_ratio : float
            Minimum spacing ratio relative to average radius.
        iterations_max : int, optional
            Maximum number of iterations. Default is 10000.
        iteration_callback : Callable, optional
            Callback function called at the start of each iteration.
            Signature: (iteration_count, fibers, boundaries) -> None.

        Raises
        ------
        RuntimeError
            If maximum iterations are exceeded without resolving overlaps.
        """
        start = time.time()
        num_diams_for_search = 4
        num_diams_for_update = 3

        # Calculate average radius for spacing logic
        if not fibers:
            return
        avg_radius = np.mean([f.radius for f in fibers])
        min_space_between_fibers = (
            min_spacing_ratio * avg_radius * 2.0
        )  # Ratio is likely relative to diameter based on name usage in original

        self._recalculate_neighbors(fibers, boundaries, num_diams_for_search)

        iteration_count = 0
        iterations_no_overlap = 0

        # Note: I changed the stopping condition from 3 to 1 to allow faster convergence.
        # This is likely fine for well-posed problems, but consider adjusting in the future.
        while iterations_no_overlap < 1 and iteration_count < iterations_max:
            if iteration_callback is not None:
                iteration_callback(iteration_count, fibers, boundaries)

            found_overlap = self._iterate_on_interference(
                fibers, boundaries, min_space_between_fibers
            )

            need_to_recalc_neighbors = False
            for fiber in fibers:
                # if any fiber has moved more than half the neighbor search
                # distance minus the radius, then we need to re-calculate the neighbors
                # Heuristic from original code
                if fiber.get_distance_since_last_neighbor_update() > fiber.radius * (
                    num_diams_for_update - 1
                ):
                    need_to_recalc_neighbors = True
                    break

            if need_to_recalc_neighbors or not found_overlap:
                self._recalculate_neighbors(fibers, boundaries, num_diams_for_search)

            if not found_overlap:
                iterations_no_overlap += 1
            else:
                iterations_no_overlap = 0

            iteration_count += 1
            if iteration_count >= iterations_max:
                print(
                    "WARNING: Maximum iterations exceeded. Stopping solve. Check input parameters to make sure an RVE is possible."
                )

        elapsed = time.time() - start
        # print("Total Time to generate RVE Geometry: " + str(elapsed) + ' seconds')
        return iteration_count

    def _recalculate_neighbors(
        self,
        fibers: List[PeriodicPrimaryFiber],
        boundaries: List[LinearBoundary],
        fiber_diams_to_search: float,
    ):
        """Recalculates ghost fibers and neighbor lists for efficient collision detection.

        Parameters
        ----------
        fibers : List[PeriodicPrimaryFiber]
            List of primary fibers.
        boundaries : List[LinearBoundary]
            List of boundaries for ghost generation.
        fiber_diams_to_search : float
            Search radius multiplier for neighbor finding.
        """
        fibers_with_ghosts: List[Fiber] = []
        for fiber in fibers:
            fiber.calc_ghost_fibers(boundaries)
            fibers_with_ghosts.append(fiber)
            fibers_with_ghosts.extend(fiber.ghost_fibers)

        fiber_centers = [f.center for f in fibers_with_ghosts]
        fiber_center_kd_tree = spatial.KDTree(fiber_centers)

        for fiber in fibers:
            fiber.update_neighbors(
                fiber_center_kd_tree, fibers_with_ghosts, fiber_diams_to_search
            )

    def _iterate_on_interference(
        self,
        fibers: List[PeriodicPrimaryFiber],
        boundaries: List[LinearBoundary],
        min_space_between_fibers: float,
    ) -> bool:
        """Performs a single pass of interference resolution on all fibers.

        Parameters
        ----------
        fibers : List[PeriodicPrimaryFiber]
            List of fibers to check.
        boundaries : List[LinearBoundary]
            Boundaries to respect during movement.
        min_space_between_fibers : float
            Minimum absolute spacing distance.

        Returns
        -------
        bool
            True if any overlap was strictly found and corrected, False otherwise.
        """
        found_overlap = False
        for fiber in fibers:
            # fix_overlap_with_neighbors returns True if it adjusted anything
            found_overlap |= fiber.fix_overlap_with_neighbors(
                boundaries, min_space_between_fibers
            )
        return found_overlap

solve_fiber_locations(fibers, boundaries, min_spacing_ratio, iterations_max, iteration_callback=None)

Iteratively resolves overlaps between fibers to ensure minimum spacing.

Parameters:

Name Type Description Default
fibers List[PeriodicPrimaryFiber]

List of fibers to overlap resolve.

required
boundaries List[LinearBoundary]

List of RVE boundaries for constraints.

required
min_spacing_ratio float

Minimum spacing ratio relative to average radius.

required
iterations_max int

Maximum number of iterations. Default is 10000.

required
iteration_callback Callable

Callback function called at the start of each iteration. Signature: (iteration_count, fibers, boundaries) -> None.

None

Raises:

Type Description
RuntimeError

If maximum iterations are exceeded without resolving overlaps.

Source code in fiber_matrix/generation/placement.py
 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
def solve_fiber_locations(
    self,
    fibers: List[PeriodicPrimaryFiber],
    boundaries: List[LinearBoundary],
    min_spacing_ratio: float,
    iterations_max: int,
    iteration_callback: Optional[
        Callable[[int, List[PeriodicPrimaryFiber], List[LinearBoundary]], None]
    ] = None,
) -> int:
    """
    Iteratively resolves overlaps between fibers to ensure minimum spacing.

    Parameters
    ----------
    fibers : List[PeriodicPrimaryFiber]
        List of fibers to overlap resolve.
    boundaries : List[LinearBoundary]
        List of RVE boundaries for constraints.
    min_spacing_ratio : float
        Minimum spacing ratio relative to average radius.
    iterations_max : int, optional
        Maximum number of iterations. Default is 10000.
    iteration_callback : Callable, optional
        Callback function called at the start of each iteration.
        Signature: (iteration_count, fibers, boundaries) -> None.

    Raises
    ------
    RuntimeError
        If maximum iterations are exceeded without resolving overlaps.
    """
    start = time.time()
    num_diams_for_search = 4
    num_diams_for_update = 3

    # Calculate average radius for spacing logic
    if not fibers:
        return
    avg_radius = np.mean([f.radius for f in fibers])
    min_space_between_fibers = (
        min_spacing_ratio * avg_radius * 2.0
    )  # Ratio is likely relative to diameter based on name usage in original

    self._recalculate_neighbors(fibers, boundaries, num_diams_for_search)

    iteration_count = 0
    iterations_no_overlap = 0

    # Note: I changed the stopping condition from 3 to 1 to allow faster convergence.
    # This is likely fine for well-posed problems, but consider adjusting in the future.
    while iterations_no_overlap < 1 and iteration_count < iterations_max:
        if iteration_callback is not None:
            iteration_callback(iteration_count, fibers, boundaries)

        found_overlap = self._iterate_on_interference(
            fibers, boundaries, min_space_between_fibers
        )

        need_to_recalc_neighbors = False
        for fiber in fibers:
            # if any fiber has moved more than half the neighbor search
            # distance minus the radius, then we need to re-calculate the neighbors
            # Heuristic from original code
            if fiber.get_distance_since_last_neighbor_update() > fiber.radius * (
                num_diams_for_update - 1
            ):
                need_to_recalc_neighbors = True
                break

        if need_to_recalc_neighbors or not found_overlap:
            self._recalculate_neighbors(fibers, boundaries, num_diams_for_search)

        if not found_overlap:
            iterations_no_overlap += 1
        else:
            iterations_no_overlap = 0

        iteration_count += 1
        if iteration_count >= iterations_max:
            print(
                "WARNING: Maximum iterations exceeded. Stopping solve. Check input parameters to make sure an RVE is possible."
            )

    elapsed = time.time() - start
    # print("Total Time to generate RVE Geometry: " + str(elapsed) + ' seconds')
    return iteration_count