fibernet 1.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. fibernet/__init__.py +167 -0
  2. fibernet/analysis/__init__.py +65 -0
  3. fibernet/analysis/advanced.py +269 -0
  4. fibernet/analysis/comparison.py +309 -0
  5. fibernet/analysis/homogenization.py +351 -0
  6. fibernet/analysis/morphology.py +145 -0
  7. fibernet/analysis/networkx_integration.py +350 -0
  8. fibernet/analysis/percolation.py +385 -0
  9. fibernet/analysis/properties.py +100 -0
  10. fibernet/analysis/spatial.py +536 -0
  11. fibernet/analysis/statistics.py +337 -0
  12. fibernet/analysis/stress_strain.py +293 -0
  13. fibernet/analysis/topology.py +463 -0
  14. fibernet/api.py +362 -0
  15. fibernet/core/__init__.py +22 -0
  16. fibernet/core/copy_utils.py +127 -0
  17. fibernet/core/crosslinks.py +630 -0
  18. fibernet/core/fiber.py +380 -0
  19. fibernet/core/material.py +290 -0
  20. fibernet/core/network.py +748 -0
  21. fibernet/core/pbc.py +246 -0
  22. fibernet/core/transform.py +500 -0
  23. fibernet/doe.py +471 -0
  24. fibernet/examples/__init__.py +0 -0
  25. fibernet/examples/basic_usage.py +104 -0
  26. fibernet/gen/__init__.py +198 -0
  27. fibernet/gen/advanced.py +750 -0
  28. fibernet/gen/bundles.py +477 -0
  29. fibernet/gen/chiral.py +319 -0
  30. fibernet/gen/curved.py +444 -0
  31. fibernet/gen/disordered.py +577 -0
  32. fibernet/gen/fractal.py +311 -0
  33. fibernet/gen/gradient.py +340 -0
  34. fibernet/gen/hierarchical.py +269 -0
  35. fibernet/gen/laminates.py +551 -0
  36. fibernet/gen/metamaterials.py +945 -0
  37. fibernet/gen/ordered.py +341 -0
  38. fibernet/gen/specialized.py +569 -0
  39. fibernet/gen/variants.py +422 -0
  40. fibernet/gen/woven.py +268 -0
  41. fibernet/integrations/__init__.py +77 -0
  42. fibernet/integrations/lammps_integration.py +342 -0
  43. fibernet/integrations/mdanalysis_integration.py +229 -0
  44. fibernet/integrations/networkx_integration.py +413 -0
  45. fibernet/integrations/ovito_integration.py +292 -0
  46. fibernet/io/__init__.py +54 -0
  47. fibernet/io/fea_export.py +454 -0
  48. fibernet/io/gmsh.py +87 -0
  49. fibernet/io/lammps.py +296 -0
  50. fibernet/io/mesh_export.py +366 -0
  51. fibernet/io/pandas_io.py +172 -0
  52. fibernet/io/pdb.py +143 -0
  53. fibernet/io/vtk.py +156 -0
  54. fibernet/io/xyz.py +84 -0
  55. fibernet/materials.py +558 -0
  56. fibernet/ml/__init__.py +35 -0
  57. fibernet/ml/dataset.py +265 -0
  58. fibernet/ml/features.py +525 -0
  59. fibernet/ml/graph_neural_network.py +523 -0
  60. fibernet/ml/predictor.py +253 -0
  61. fibernet/py.typed +0 -0
  62. fibernet/pyvista_viz.py +430 -0
  63. fibernet/sim/__init__.py +244 -0
  64. fibernet/sim/accelerated.py +716 -0
  65. fibernet/sim/acoustic.py +473 -0
  66. fibernet/sim/buckling_analysis.py +460 -0
  67. fibernet/sim/coupled.py +556 -0
  68. fibernet/sim/coupling.py +136 -0
  69. fibernet/sim/creep.py +386 -0
  70. fibernet/sim/cte.py +276 -0
  71. fibernet/sim/damage.py +553 -0
  72. fibernet/sim/diffusion.py +354 -0
  73. fibernet/sim/dma.py +400 -0
  74. fibernet/sim/dynamics.py +477 -0
  75. fibernet/sim/electromagnetic.py +229 -0
  76. fibernet/sim/fatigue.py +429 -0
  77. fibernet/sim/fluid.py +533 -0
  78. fibernet/sim/fracture.py +132 -0
  79. fibernet/sim/fracture_mechanics.py +505 -0
  80. fibernet/sim/incremental_fem.py +499 -0
  81. fibernet/sim/mechanical.py +787 -0
  82. fibernet/sim/molecular_dynamics.py +376 -0
  83. fibernet/sim/multiscale.py +537 -0
  84. fibernet/sim/nonlinear.py +924 -0
  85. fibernet/sim/optimization.py +361 -0
  86. fibernet/sim/periodic.py +377 -0
  87. fibernet/sim/permeability.py +536 -0
  88. fibernet/sim/rheology.py +471 -0
  89. fibernet/sim/thermal.py +216 -0
  90. fibernet/sim/uncertainty.py +346 -0
  91. fibernet/sim/viscoelastic.py +328 -0
  92. fibernet/trimesh_integration.py +387 -0
  93. fibernet/units.py +323 -0
  94. fibernet/utils/__init__.py +27 -0
  95. fibernet/utils/batch.py +246 -0
  96. fibernet/utils/config.py +614 -0
  97. fibernet/utils/ensemble.py +441 -0
  98. fibernet/utils/exceptions.py +123 -0
  99. fibernet/utils/geometry.py +103 -0
  100. fibernet/utils/io.py +78 -0
  101. fibernet/utils/parametric.py +322 -0
  102. fibernet/utils/units.py +204 -0
  103. fibernet/utils/validation.py +477 -0
  104. fibernet/version.py +11 -0
  105. fibernet/visualization.py +438 -0
  106. fibernet/viz/__init__.py +42 -0
  107. fibernet/viz/advanced.py +346 -0
  108. fibernet/viz/animate.py +74 -0
  109. fibernet/viz/plot2d.py +188 -0
  110. fibernet/viz/plotly_viz.py +341 -0
  111. fibernet/viz/render3d.py +165 -0
  112. fibernet/viz/visualization.py +527 -0
  113. fibernet-1.24.0.dist-info/METADATA +452 -0
  114. fibernet-1.24.0.dist-info/RECORD +117 -0
  115. fibernet-1.24.0.dist-info/WHEEL +5 -0
  116. fibernet-1.24.0.dist-info/licenses/LICENSE +21 -0
  117. fibernet-1.24.0.dist-info/top_level.txt +1 -0
fibernet/__init__.py ADDED
@@ -0,0 +1,167 @@
1
+ """
2
+ FiberNet - A comprehensive toolkit for fiber network structure research.
3
+
4
+ Provides tools for:
5
+ - Generation: 2D/3D fiber networks (ordered, disordered, chiral, bundled, woven, hierarchical)
6
+ - Simulation: Mechanical, dynamics, fracture, thermal, electromagnetic, fluid, acoustic
7
+ - Analysis: Topology, morphology, properties, percolation, multi-scale
8
+ - Visualization: 3D rendering, animation, 2D plots, interactive
9
+ - Machine Learning: Feature extraction, GNN models
10
+
11
+ Homepage: https://ml-biomat.com
12
+ GitHub: https://github.com/GellmanSparrowS/fibernet
13
+
14
+ Quick Start
15
+ -----------
16
+ >>> import fibernet as fn
17
+ >>> # Create a random 2D network
18
+ >>> net = fn.create("random_2d", num_fibers=100, fiber_length=10.0, box_size=(20, 20))
19
+ >>> # Analyze structure
20
+ >>> stats = fn.analyze(net)
21
+ >>> # Run mechanical simulation
22
+ >>> result = fn.simulate_mechanics(net, strain=0.01)
23
+ >>> # Visualize
24
+ >>> fn.plot(net)
25
+
26
+ For more examples, see the tutorials/ directory.
27
+ """
28
+
29
+ __version__ = "1.24.0"
30
+ __author__ = "ML-BioMat Lab"
31
+
32
+ # Core data structures
33
+ from fibernet.core.fiber import Fiber
34
+ from fibernet.core.network import FiberNetwork
35
+ from fibernet.core.material import Material
36
+ from fibernet.core.copy_utils import copy_fiber, copy_material, copy_network
37
+
38
+ # High-level convenience API
39
+ from .api import (
40
+ create, mirror, rotate, scale, translate, merge, tile,
41
+ simulate_mechanics, simulate_thermal, analyze, export, load, plot,
42
+ )
43
+
44
+ # Version information
45
+ from .version import __version__, __author__, __license__
46
+
47
+ # Simulation modules (lazy imports for optional dependencies)
48
+ from .sim.fluid import DarcySolver, PoreNetworkModel
49
+ from .sim.acoustic import AcousticSolver
50
+
51
+ # Visualization module
52
+ from . import viz
53
+
54
+ # ML module (optional)
55
+ try:
56
+ from . import ml
57
+ except ImportError:
58
+ ml = None
59
+
60
+ # Submodules for advanced usage
61
+ from . import gen # Network generators
62
+ from . import sim # Simulators
63
+ from . import analysis # Analyzers
64
+ from . import io # I/O utilities
65
+ from . import utils # Utilities
66
+
67
+ __all__ = [
68
+ # Core classes
69
+ "Fiber",
70
+ "FiberNetwork",
71
+ "Material",
72
+ "copy_fiber",
73
+ "copy_material",
74
+ "copy_network",
75
+
76
+ # High-level API
77
+ "create",
78
+ "mirror",
79
+ "rotate",
80
+ "scale",
81
+ "translate",
82
+ "merge",
83
+ "tile",
84
+ "simulate_mechanics",
85
+ "simulate_thermal",
86
+ "analyze",
87
+ "export",
88
+ "load",
89
+ "plot",
90
+
91
+ # Simulators
92
+ "DarcySolver",
93
+ "PoreNetworkModel",
94
+ "AcousticSolver",
95
+
96
+ # Submodules
97
+ "gen",
98
+ "sim",
99
+ "analysis",
100
+ "io",
101
+ "viz",
102
+ "ml",
103
+ "utils",
104
+
105
+ # Metadata
106
+ "__version__",
107
+ "__author__",
108
+ "__license__",
109
+ ]
110
+
111
+ # Visualization
112
+ from .visualization import (
113
+ NetworkVisualizer, PlotStyle, visualize_network
114
+ )
115
+ __all__.extend([
116
+ "NetworkVisualizer", "PlotStyle", "visualize_network"
117
+ ])
118
+
119
+ # Design of Experiments
120
+ from .doe import (
121
+ DesignOfExperiments, ExperimentDesign, ExperimentResult, SweepResult,
122
+ run_parameter_sweep
123
+ )
124
+ __all__.extend([
125
+ "DesignOfExperiments", "ExperimentDesign", "ExperimentResult", "SweepResult",
126
+ "run_parameter_sweep"
127
+ ])
128
+
129
+ # Materials database
130
+ from .materials import (
131
+ get_material, list_materials, compare_materials, get_material_database, m
132
+ )
133
+ __all__.extend([
134
+ "get_material", "list_materials", "compare_materials", "get_material_database", "m"
135
+ ])
136
+
137
+ # Unit conversions
138
+ from .units import (
139
+ convert_length, convert_force, convert_pressure, convert_temperature,
140
+ convert_energy, parse_unit_string, scale_network_properties
141
+ )
142
+ __all__.extend([
143
+ "convert_length", "convert_force", "convert_pressure", "convert_temperature",
144
+ "convert_energy", "parse_unit_string", "scale_network_properties"
145
+ ])
146
+
147
+ # PyVista visualization (lazy import to avoid VTK segfaults in CI)
148
+ import os as _os
149
+ if _os.environ.get("CI") != "true":
150
+ from .pyvista_viz import PyVistaVisualizer, visualize_network_3d, PYVISTA_AVAILABLE
151
+ else:
152
+ PYVISTA_AVAILABLE = False
153
+ PyVistaVisualizer = None
154
+ visualize_network_3d = None
155
+ __all__.extend([
156
+ "PyVistaVisualizer", "visualize_network_3d", "PYVISTA_AVAILABLE"
157
+ ])
158
+
159
+ # Trimesh integration
160
+ from .trimesh_integration import (
161
+ TrimeshConverter, network_to_trimesh, analyze_mesh_properties,
162
+ boolean_operation, repair_mesh, simplify_mesh, TRIMESH_AVAILABLE
163
+ )
164
+ __all__.extend([
165
+ "TrimeshConverter", "network_to_trimesh", "analyze_mesh_properties",
166
+ "boolean_operation", "repair_mesh", "simplify_mesh", "TRIMESH_AVAILABLE"
167
+ ])
@@ -0,0 +1,65 @@
1
+ """
2
+ Analysis tools for fiber networks.
3
+ 分析工具模块,用于纤维网络结构分析。
4
+
5
+ Submodules / 子模块:
6
+ - topology: Graph-theoretic analysis / 图论分析
7
+ - morphology: Geometric characterization / 几何表征
8
+ - properties: Effective property estimation / 有效性能估算
9
+ - advanced: Spectral analysis, pore distribution, anisotropy, fingerprinting
10
+ - stress_strain: Stress-strain curve extraction and analysis
11
+ """
12
+
13
+ from fibernet.analysis.morphology import MorphologyAnalyzer
14
+ from fibernet.analysis.properties import PropertyEstimator
15
+
16
+ # Topology requires networkx; guard import for CI minimal installs
17
+ try:
18
+ from fibernet.analysis.topology import TopologyAnalyzer
19
+ except (ImportError, NameError):
20
+ TopologyAnalyzer = None
21
+
22
+ try:
23
+ from fibernet.analysis.advanced import (
24
+ SpectralAnalyzer, PoreAnalyzer, AnisotropyAnalyzer, StructuralFingerprint,
25
+ )
26
+ except ImportError:
27
+ pass
28
+
29
+ try:
30
+ from fibernet.analysis.stress_strain import (
31
+ StressStrainCurve, extract_stress_strain, compare_curves,
32
+ )
33
+ except ImportError:
34
+ pass
35
+
36
+ __all__ = [
37
+ "MorphologyAnalyzer", "PropertyEstimator",
38
+ ]
39
+
40
+ if TopologyAnalyzer is not None:
41
+ __all__.append("TopologyAnalyzer")
42
+
43
+ # Statistical analysis
44
+ from fibernet.analysis.statistics import StatisticalAnalyzer
45
+
46
+ # NetworkX integration
47
+ try:
48
+ from fibernet.analysis.networkx_integration import (
49
+ to_networkx, compute_centrality, detect_communities,
50
+ compute_graph_metrics, find_shortest_path, compute_small_world_metrics,
51
+ )
52
+ except ImportError:
53
+ pass
54
+
55
+ __all__ += [
56
+ "StatisticalAnalyzer",
57
+ ]
58
+
59
+ # Percolation analysis
60
+ from fibernet.analysis.percolation import (
61
+ PercolationAnalyzer, PercolationResult, estimate_percolation_threshold,
62
+ )
63
+ __all__ += [
64
+ "PercolationAnalyzer", "PercolationResult", "estimate_percolation_threshold",
65
+ ]
@@ -0,0 +1,269 @@
1
+ """
2
+ Advanced analysis tools for fiber networks.
3
+
4
+ Provides:
5
+ - Spectral analysis (eigenvalues of graph Laplacian)
6
+ - Pore size distribution estimation
7
+ - Percolation threshold finding
8
+ - Anisotropy tensor analysis
9
+ - Structural fingerprinting
10
+ """
11
+
12
+ import numpy as np
13
+ from typing import Dict, List, Tuple, Optional
14
+ from fibernet.core.network import FiberNetwork
15
+
16
+
17
+ class SpectralAnalyzer:
18
+ """Spectral graph analysis of fiber networks."""
19
+
20
+ def __init__(self, network: FiberNetwork):
21
+ self.network = network
22
+ self._graph = None
23
+ self._laplacian = None
24
+
25
+ @property
26
+ def graph(self):
27
+ if self._graph is None:
28
+ self._graph = self.network.to_networkx()
29
+ return self._graph
30
+
31
+ def laplacian_eigenvalues(self, k: int = None) -> np.ndarray:
32
+ """Compute eigenvalues of the graph Laplacian.
33
+
34
+ Parameters
35
+ ----------
36
+ k : int, optional
37
+ Number of eigenvalues to compute. Defaults to min(n, 20).
38
+ """
39
+ import networkx as nx
40
+
41
+ n = len(self.graph.nodes)
42
+ if n < 2:
43
+ return np.array([])
44
+
45
+ if k is None:
46
+ k = min(n - 1, 20)
47
+
48
+ L = nx.laplacian_matrix(self.graph).astype(float)
49
+
50
+ if n <= 100:
51
+ eigenvalues = np.linalg.eigvalsh(L.toarray())
52
+ else:
53
+ from scipy.sparse.linalg import eigsh
54
+ eigenvalues = eigsh(L, k=min(k, n - 1), which='SM', return_eigenvectors=False)
55
+
56
+ return np.sort(eigenvalues)
57
+
58
+ def spectral_gap(self) -> float:
59
+ """Spectral gap (second smallest Laplacian eigenvalue).
60
+
61
+ Related to algebraic connectivity and network robustness.
62
+ """
63
+ eigs = self.laplacian_eigenvalues(k=5)
64
+ if len(eigs) >= 2:
65
+ return float(eigs[1])
66
+ return 0.0
67
+
68
+ def spectral_entropy(self) -> float:
69
+ """Spectral entropy of the Laplacian spectrum.
70
+
71
+ Measures the complexity/disorder of the network structure.
72
+ """
73
+ eigs = self.laplacian_eigenvalues()
74
+ if len(eigs) == 0:
75
+ return 0.0
76
+
77
+ eigs = np.maximum(eigs, 1e-12)
78
+ p = eigs / eigs.sum()
79
+ entropy = -np.sum(p * np.log(p + 1e-12))
80
+ return float(entropy)
81
+
82
+ def algebraic_connectivity(self) -> float:
83
+ """Algebraic connectivity (Fiedler value).
84
+
85
+ Higher values indicate better-connected networks.
86
+ """
87
+ return self.spectral_gap()
88
+
89
+
90
+ class PoreAnalyzer:
91
+ """Estimate pore size distribution in fiber networks."""
92
+
93
+ def __init__(self, network: FiberNetwork):
94
+ self.network = network
95
+
96
+ def pore_size_distribution(
97
+ self,
98
+ num_samples: int = 1000,
99
+ seed: Optional[int] = None,
100
+ ) -> Tuple[np.ndarray, np.ndarray]:
101
+ """Estimate pore size distribution using random sampling.
102
+
103
+ For each random point in the bounding box, finds the
104
+ distance to the nearest fiber surface.
105
+
106
+ Returns
107
+ -------
108
+ radii : np.ndarray
109
+ Sampled pore radii.
110
+ histogram : np.ndarray
111
+ Normalized histogram values.
112
+ """
113
+ rng = np.random.default_rng(seed)
114
+
115
+ if self.network.num_fibers == 0:
116
+ return np.array([]), np.array([])
117
+
118
+ bb_min, bb_max = self.network.bounding_box()
119
+
120
+ from scipy.spatial import cKDTree
121
+
122
+ all_points = []
123
+ for fiber in self.network.fibers:
124
+ for pt in fiber.centerline:
125
+ all_points.append(pt)
126
+
127
+ all_points = np.array(all_points)
128
+ tree = cKDTree(all_points)
129
+
130
+ samples = rng.uniform(bb_min, bb_max, (num_samples, 3))
131
+
132
+ distances, indices = tree.query(samples)
133
+
134
+ pore_radii = np.zeros(num_samples)
135
+ for i, (dist, idx) in enumerate(zip(distances, indices)):
136
+ fiber_idx = 0
137
+ cum_pts = 0
138
+ for f in self.network.fibers:
139
+ if cum_pts + len(f.centerline) > idx:
140
+ fiber_idx = f.fiber_id
141
+ break
142
+ cum_pts += len(f.centerline)
143
+
144
+ if fiber_idx < len(self.network.fibers):
145
+ r_fiber = self.network.fibers[fiber_idx].radius
146
+ pore_radii[i] = max(dist - r_fiber, 0)
147
+ else:
148
+ pore_radii[i] = dist
149
+
150
+ return pore_radii, pore_radii
151
+
152
+ def mean_pore_size(self) -> float:
153
+ """Mean pore radius."""
154
+ radii, _ = self.pore_size_distribution(num_samples=500, seed=42)
155
+ if len(radii) == 0:
156
+ return 0.0
157
+ return float(np.mean(radii))
158
+
159
+ def pore_size_statistics(self) -> Dict[str, float]:
160
+ """Comprehensive pore size statistics."""
161
+ radii, _ = self.pore_size_distribution(num_samples=500, seed=42)
162
+ if len(radii) == 0:
163
+ return {"mean": 0, "median": 0, "std": 0, "max": 0}
164
+ return {
165
+ "mean": float(np.mean(radii)),
166
+ "median": float(np.median(radii)),
167
+ "std": float(np.std(radii)),
168
+ "max": float(np.max(radii)),
169
+ "min": float(np.min(radii[radii > 0])) if np.any(radii > 0) else 0,
170
+ "p95": float(np.percentile(radii, 95)),
171
+ }
172
+
173
+
174
+ class AnisotropyAnalyzer:
175
+ """Analyze structural anisotropy of fiber networks."""
176
+
177
+ def __init__(self, network: FiberNetwork):
178
+ self.network = network
179
+
180
+ def orientation_tensor(self) -> np.ndarray:
181
+ """Compute the second-order orientation tensor.
182
+
183
+ A_ij = <p_i * p_j> where p is the fiber direction vector.
184
+
185
+ For isotropic: A = I/3 (3D) or I/2 (2D)
186
+ """
187
+ orientations = self.network.fiber_orientations()
188
+ if len(orientations) == 0:
189
+ return np.eye(3) / 3
190
+
191
+ n = len(orientations)
192
+ A = np.zeros((3, 3))
193
+ for o in orientations:
194
+ A += np.outer(o, o)
195
+ A /= n
196
+
197
+ return A
198
+
199
+ def anisotropy_index(self) -> float:
200
+ """Anisotropy index based on orientation tensor eigenvalues.
201
+
202
+ Returns value between 0 (isotropic) and 1 (fully aligned).
203
+ """
204
+ A = self.orientation_tensor()
205
+ eigenvalues = np.sort(np.linalg.eigvalsh(A))[::-1]
206
+
207
+ if len(eigenvalues) < 3:
208
+ return 0.0
209
+
210
+ a1 = eigenvalues[0]
211
+ iso = 1.0 / 3
212
+
213
+ ai = (a1 - iso) / (1 - iso) if iso < 1 else 0
214
+ return float(max(0, min(1, ai)))
215
+
216
+ def principal_directions(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
217
+ """Get principal directions of fiber orientation."""
218
+ A = self.orientation_tensor()
219
+ eigenvalues, eigenvectors = np.linalg.eigh(A)
220
+
221
+ idx = np.argsort(eigenvalues)[::-1]
222
+ return eigenvectors[:, idx], eigenvalues[idx], eigenvectors[:, idx]
223
+
224
+
225
+ class StructuralFingerprint:
226
+ """Create structural fingerprints for network comparison."""
227
+
228
+ def __init__(self, network: FiberNetwork):
229
+ self.network = network
230
+
231
+ def compute_fingerprint(self) -> Dict[str, float]:
232
+ """Compute a compact structural fingerprint vector."""
233
+ from fibernet.analysis.morphology import MorphologyAnalyzer
234
+ from fibernet.analysis.topology import TopologyAnalyzer
235
+
236
+ morph = MorphologyAnalyzer(self.network)
237
+ topo = TopologyAnalyzer(self.network)
238
+
239
+ fp = {
240
+ "num_fibers": float(self.network.num_fibers),
241
+ "density": self.network.density(),
242
+ "mean_length": self.network.mean_fiber_length,
243
+ "nematic_order": morph.nematic_order_parameter(),
244
+ "mean_tortuosity": float(np.mean(morph.tortuosity_distribution())),
245
+ "mean_degree": topo.degree_statistics()["mean"],
246
+ "clustering": topo.clustering_coefficient(),
247
+ }
248
+
249
+ try:
250
+ spec = SpectralAnalyzer(self.network)
251
+ fp["spectral_gap"] = spec.spectral_gap()
252
+ except:
253
+ fp["spectral_gap"] = 0.0
254
+
255
+ return fp
256
+
257
+ def distance_to(self, other: "StructuralFingerprint") -> float:
258
+ """Compute Euclidean distance between fingerprints."""
259
+ fp1 = self.compute_fingerprint()
260
+ fp2 = other.compute_fingerprint()
261
+
262
+ keys = sorted(set(fp1.keys()) & set(fp2.keys()))
263
+ v1 = np.array([fp1[k] for k in keys])
264
+ v2 = np.array([fp2[k] for k in keys])
265
+
266
+ v1_norm = v1 / (np.linalg.norm(v1) + 1e-12)
267
+ v2_norm = v2 / (np.linalg.norm(v2) + 1e-12)
268
+
269
+ return float(np.linalg.norm(v1_norm - v2_norm))