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.
- fibernet/__init__.py +167 -0
- fibernet/analysis/__init__.py +65 -0
- fibernet/analysis/advanced.py +269 -0
- fibernet/analysis/comparison.py +309 -0
- fibernet/analysis/homogenization.py +351 -0
- fibernet/analysis/morphology.py +145 -0
- fibernet/analysis/networkx_integration.py +350 -0
- fibernet/analysis/percolation.py +385 -0
- fibernet/analysis/properties.py +100 -0
- fibernet/analysis/spatial.py +536 -0
- fibernet/analysis/statistics.py +337 -0
- fibernet/analysis/stress_strain.py +293 -0
- fibernet/analysis/topology.py +463 -0
- fibernet/api.py +362 -0
- fibernet/core/__init__.py +22 -0
- fibernet/core/copy_utils.py +127 -0
- fibernet/core/crosslinks.py +630 -0
- fibernet/core/fiber.py +380 -0
- fibernet/core/material.py +290 -0
- fibernet/core/network.py +748 -0
- fibernet/core/pbc.py +246 -0
- fibernet/core/transform.py +500 -0
- fibernet/doe.py +471 -0
- fibernet/examples/__init__.py +0 -0
- fibernet/examples/basic_usage.py +104 -0
- fibernet/gen/__init__.py +198 -0
- fibernet/gen/advanced.py +750 -0
- fibernet/gen/bundles.py +477 -0
- fibernet/gen/chiral.py +319 -0
- fibernet/gen/curved.py +444 -0
- fibernet/gen/disordered.py +577 -0
- fibernet/gen/fractal.py +311 -0
- fibernet/gen/gradient.py +340 -0
- fibernet/gen/hierarchical.py +269 -0
- fibernet/gen/laminates.py +551 -0
- fibernet/gen/metamaterials.py +945 -0
- fibernet/gen/ordered.py +341 -0
- fibernet/gen/specialized.py +569 -0
- fibernet/gen/variants.py +422 -0
- fibernet/gen/woven.py +268 -0
- fibernet/integrations/__init__.py +77 -0
- fibernet/integrations/lammps_integration.py +342 -0
- fibernet/integrations/mdanalysis_integration.py +229 -0
- fibernet/integrations/networkx_integration.py +413 -0
- fibernet/integrations/ovito_integration.py +292 -0
- fibernet/io/__init__.py +54 -0
- fibernet/io/fea_export.py +454 -0
- fibernet/io/gmsh.py +87 -0
- fibernet/io/lammps.py +296 -0
- fibernet/io/mesh_export.py +366 -0
- fibernet/io/pandas_io.py +172 -0
- fibernet/io/pdb.py +143 -0
- fibernet/io/vtk.py +156 -0
- fibernet/io/xyz.py +84 -0
- fibernet/materials.py +558 -0
- fibernet/ml/__init__.py +35 -0
- fibernet/ml/dataset.py +265 -0
- fibernet/ml/features.py +525 -0
- fibernet/ml/graph_neural_network.py +523 -0
- fibernet/ml/predictor.py +253 -0
- fibernet/py.typed +0 -0
- fibernet/pyvista_viz.py +430 -0
- fibernet/sim/__init__.py +244 -0
- fibernet/sim/accelerated.py +716 -0
- fibernet/sim/acoustic.py +473 -0
- fibernet/sim/buckling_analysis.py +460 -0
- fibernet/sim/coupled.py +556 -0
- fibernet/sim/coupling.py +136 -0
- fibernet/sim/creep.py +386 -0
- fibernet/sim/cte.py +276 -0
- fibernet/sim/damage.py +553 -0
- fibernet/sim/diffusion.py +354 -0
- fibernet/sim/dma.py +400 -0
- fibernet/sim/dynamics.py +477 -0
- fibernet/sim/electromagnetic.py +229 -0
- fibernet/sim/fatigue.py +429 -0
- fibernet/sim/fluid.py +533 -0
- fibernet/sim/fracture.py +132 -0
- fibernet/sim/fracture_mechanics.py +505 -0
- fibernet/sim/incremental_fem.py +499 -0
- fibernet/sim/mechanical.py +787 -0
- fibernet/sim/molecular_dynamics.py +376 -0
- fibernet/sim/multiscale.py +537 -0
- fibernet/sim/nonlinear.py +924 -0
- fibernet/sim/optimization.py +361 -0
- fibernet/sim/periodic.py +377 -0
- fibernet/sim/permeability.py +536 -0
- fibernet/sim/rheology.py +471 -0
- fibernet/sim/thermal.py +216 -0
- fibernet/sim/uncertainty.py +346 -0
- fibernet/sim/viscoelastic.py +328 -0
- fibernet/trimesh_integration.py +387 -0
- fibernet/units.py +323 -0
- fibernet/utils/__init__.py +27 -0
- fibernet/utils/batch.py +246 -0
- fibernet/utils/config.py +614 -0
- fibernet/utils/ensemble.py +441 -0
- fibernet/utils/exceptions.py +123 -0
- fibernet/utils/geometry.py +103 -0
- fibernet/utils/io.py +78 -0
- fibernet/utils/parametric.py +322 -0
- fibernet/utils/units.py +204 -0
- fibernet/utils/validation.py +477 -0
- fibernet/version.py +11 -0
- fibernet/visualization.py +438 -0
- fibernet/viz/__init__.py +42 -0
- fibernet/viz/advanced.py +346 -0
- fibernet/viz/animate.py +74 -0
- fibernet/viz/plot2d.py +188 -0
- fibernet/viz/plotly_viz.py +341 -0
- fibernet/viz/render3d.py +165 -0
- fibernet/viz/visualization.py +527 -0
- fibernet-1.24.0.dist-info/METADATA +452 -0
- fibernet-1.24.0.dist-info/RECORD +117 -0
- fibernet-1.24.0.dist-info/WHEEL +5 -0
- fibernet-1.24.0.dist-info/licenses/LICENSE +21 -0
- 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))
|