@@ -2315,6 +2315,7 @@ class SimplicialComplex(ChainComplex):
23152315 _simplices_table : Dict [int , List [Tuple [int , ...]]] = PrivateAttr (default_factory = dict )
23162316 _point_cloud_to_simplices : Dict [int , List [Tuple [int , ...]]] = PrivateAttr (default_factory = dict )
23172317 _simplices_to_point_cloud : Dict [Tuple [int , ...], np .ndarray ] = PrivateAttr (default_factory = dict )
2318+ _point_cloud_cache : Optional [Any ] = PrivateAttr (default = None )
23182319 coefficient_ring : str = "Z"
23192320 filtration : Dict [Tuple [int , ...], float ] = Field (default_factory = dict )
23202321
@@ -3149,8 +3150,13 @@ def simplices_dict(self) -> Dict[int, List[Tuple[int, ...]]]:
31493150 def point_cloud (self ) -> Optional ["PointCloud" ]:
31503151 """Return the PointCloud wrapper for coordinates, if coordinates exist."""
31513152 if hasattr (self , "_coordinates" ) and self ._coordinates is not None :
3153+ if getattr (self , "_point_cloud_cache" , None ) is not None :
3154+ if self ._point_cloud_cache .points is self ._coordinates :
3155+ return self ._point_cloud_cache
31523156 from ..geometry .point_cloud import PointCloud
3153- return PointCloud (self ._coordinates , parent = self )
3157+ pc = PointCloud (self ._coordinates , parent = self )
3158+ self ._point_cloud_cache = pc
3159+ return pc
31543160 return None
31553161
31563162 @point_cloud .setter
@@ -3160,15 +3166,18 @@ def point_cloud(self, pc: Optional[Union["PointCloud", np.ndarray]]) -> None:
31603166 self ._coordinates = None
31613167 self ._point_cloud_to_simplices = {}
31623168 self ._simplices_to_point_cloud = {}
3169+ self ._point_cloud_cache = None
31633170 else :
31643171 from ..geometry .point_cloud import PointCloud
31653172 if isinstance (pc , PointCloud ):
31663173 pts = pc .points
3174+ self ._point_cloud_cache = pc
31673175 else :
31683176 pts = np .asarray (pc , dtype = np .float64 )
3177+ self ._point_cloud_cache = PointCloud (pts , parent = self )
31693178 self ._coordinates = pts
31703179 self ._generate_point_cloud_mappings (pts )
3171- self ._link_point_cloud (pc )
3180+ self ._link_point_cloud (self . _point_cloud_cache )
31723181
31733182 @property
31743183 def point_cloud_to_simplices (self ) -> Dict [int , List [Tuple [int , ...]]]:
@@ -3202,7 +3211,102 @@ def _link_point_cloud(self, points: Any) -> None:
32023211 if isinstance (points , PointCloud ):
32033212 points ._parent = self
32043213
3214+ def verify_transformation_collision (self , tol : float = 1e-8 ) -> List [Dict [str , Any ]]:
3215+ """Verifies if any self-intersections (collisions) occurred during the transformation history.
3216+
3217+ This method walks through the sequential history of geometric deformations applied
3218+ to the linked PointCloud. At each step (including the initial undeformed state), it
3219+ recreates the coordinate realization, constructs a piecewise-linear map (PLMap),
3220+ and runs broad-phase/narrow-phase intersection detection to identify any overlaps
3221+ between non-adjacent simplices.
3222+
3223+ Algorithm & Mathematical Foundations:
3224+ 1. Initializes a temporary, parent-less `PointCloud` using the `original_points`
3225+ coordinates of the linked point cloud.
3226+ 2. For each state (Step 0 up to Step N):
3227+ - Constructs a PLMap f: |K| -> R^D using the active coordinates.
3228+ - Checks for self-intersections by running `detect_self_intersections(pl_map)`.
3229+ An intersection is detected if the realizations of two non-adjacent simplices
3230+ intersect in ambient space within the given numerical tolerance.
3231+ - If intersections are found, records details about the colliding simplices
3232+ and the ambient coordinates of their vertices.
3233+ - Applies the next transformation in the history sequence to the coordinates.
3234+ 3. Returns the log of all detected collisions.
3235+
3236+ Args:
3237+ tol (float): Numerical tolerance for intersection tests (distance below which
3238+ simplices are considered to overlap). Defaults to 1e-8.
3239+
3240+ Returns:
3241+ List[Dict[str, Any]]: A list of dictionaries representing steps where collisions
3242+ were detected. Each dictionary contains:
3243+ - "step" (int): The step index (0 for initial state, 1..N for transformations).
3244+ - "method" (str): The name of the transformation applied (or "initial_state").
3245+ - "args" (Dict[str, Any]): The arguments supplied to the transformation.
3246+ - "witnesses" (List[Dict[str, Any]]): Details of each collision witness, including:
3247+ - "simplex_a" (Tuple[int, ...]): Vertices of the first simplex.
3248+ - "simplex_b" (Tuple[int, ...]): Vertices of the second simplex.
3249+ - "simplex_a_coordinates" (List[List[float]]): Ambient coordinates of simplex_a vertices.
3250+ - "simplex_b_coordinates" (List[List[float]]): Ambient coordinates of simplex_b vertices.
3251+ - "kind" (str): Intersection type (e.g. 'segment_segment').
3252+ - "distance" (float): Minimum distance between the simplices.
3253+ - "overlap_dimension" (int): Dimension of the intersection set.
3254+ - "notes" (List[str]): Diagnostic notes.
3255+
3256+ Raises:
3257+ ValueError: If the simplicial complex does not have a linked PointCloud/coordinates.
3258+ """
3259+ pc = self .point_cloud
3260+ if pc is None :
3261+ raise ValueError ("SimplicialComplex has no coordinates/PointCloud linked." )
3262+
3263+ history = pc ._history
3264+ original_pts = pc ._original_points
3265+
3266+ from ..geometry .point_cloud import PointCloud
3267+ from ..geometry .embedding import PLMap , detect_self_intersections
3268+
3269+ temp_pc = PointCloud (original_pts .copy (), original_points = original_pts )
3270+ collisions = []
3271+
3272+ def check_collisions (step_idx : int , method_name : str , args : Dict [str , Any ]) -> None :
3273+ pl_map = PLMap .from_source (self , coordinates = temp_pc .points )
3274+ report = detect_self_intersections (pl_map , tol = tol )
3275+ if report .has_intersections :
3276+ witness_list = []
3277+ for w in report .witnesses :
3278+ witness_list .append ({
3279+ "simplex_a" : w .simplex_a ,
3280+ "simplex_b" : w .simplex_b ,
3281+ "simplex_a_coordinates" : temp_pc .points [list (w .simplex_a )].tolist (),
3282+ "simplex_b_coordinates" : temp_pc .points [list (w .simplex_b )].tolist (),
3283+ "kind" : w .kind ,
3284+ "distance" : w .distance ,
3285+ "overlap_dimension" : w .overlap_dimension ,
3286+ "notes" : w .notes ,
3287+ })
3288+ collisions .append ({
3289+ "step" : step_idx ,
3290+ "method" : method_name ,
3291+ "args" : args ,
3292+ "witnesses" : witness_list ,
3293+ })
3294+
3295+ # Check initial state (Step 0)
3296+ check_collisions (step_idx = 0 , method_name = "initial_state" , args = {})
3297+
3298+ # Apply transformations sequentially and check each step
3299+ for i , entry in enumerate (history ):
3300+ method_name = entry ["method" ]
3301+ args = entry ["args" ]
3302+ method = getattr (temp_pc , method_name )
3303+ method (** args )
3304+ check_collisions (step_idx = i + 1 , method_name = method_name , args = args )
3305+
3306+ return collisions
3307+
32053308 @classmethod
3309+
32063310 def concatenate (cls , complexes : Iterable ["SimplicialComplex" ]) -> "SimplicialComplex" :
32073311 """Concatenate multiple simplicial complexes (simplex trees) into a single larger complex.
32083312
0 commit comments