voids 0.1.4__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.
- voids/__init__.py +8 -0
- voids/_logging.py +5 -0
- voids/_testing.py +25 -0
- voids/benchmarks/__init__.py +29 -0
- voids/benchmarks/_shared.py +92 -0
- voids/benchmarks/crosscheck.py +353 -0
- voids/benchmarks/segmented_volume.py +261 -0
- voids/benchmarks/xlb.py +1045 -0
- voids/core/__init__.py +6 -0
- voids/core/network.py +176 -0
- voids/core/provenance.py +83 -0
- voids/core/sample.py +165 -0
- voids/core/validation.py +140 -0
- voids/examples/__init__.py +13 -0
- voids/examples/demo.py +117 -0
- voids/examples/manufactured.py +73 -0
- voids/examples/mesh.py +283 -0
- voids/generators/__init__.py +63 -0
- voids/generators/network.py +825 -0
- voids/generators/porous_image.py +717 -0
- voids/generators/vug_templates.py +373 -0
- voids/geom/__init__.py +14 -0
- voids/geom/characteristic.py +103 -0
- voids/geom/hydraulic.py +717 -0
- voids/graph/__init__.py +15 -0
- voids/graph/connectivity.py +244 -0
- voids/graph/incidence.py +34 -0
- voids/graph/metrics.py +96 -0
- voids/image/__init__.py +30 -0
- voids/image/_utils.py +76 -0
- voids/image/connectivity.py +97 -0
- voids/image/network_extraction.py +265 -0
- voids/image/segmentation.py +362 -0
- voids/io/__init__.py +13 -0
- voids/io/hdf5.py +160 -0
- voids/io/openpnm.py +147 -0
- voids/io/porespy.py +669 -0
- voids/linalg/__init__.py +0 -0
- voids/linalg/assemble.py +58 -0
- voids/linalg/backends.py +28 -0
- voids/linalg/bc.py +67 -0
- voids/linalg/diagnostics.py +26 -0
- voids/linalg/solve.py +48 -0
- voids/paths.py +110 -0
- voids/physics/__init__.py +0 -0
- voids/physics/petrophysics.py +174 -0
- voids/physics/singlephase.py +350 -0
- voids/physics/transport.py +1 -0
- voids/py.typed +0 -0
- voids/simulators/__init__.py +5 -0
- voids/simulators/run_singlephase.py +38 -0
- voids/version.py +1 -0
- voids/visualization/__init__.py +6 -0
- voids/visualization/_sizing.py +88 -0
- voids/visualization/plotly.py +370 -0
- voids/visualization/pyvista.py +305 -0
- voids-0.1.4.dist-info/METADATA +266 -0
- voids-0.1.4.dist-info/RECORD +60 -0
- voids-0.1.4.dist-info/WHEEL +4 -0
- voids-0.1.4.dist-info/licenses/LICENSE +504 -0
voids/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""voids: pore network modeling scientific toolkit (v0.1.x)."""
|
|
2
|
+
|
|
3
|
+
from voids.version import __version__
|
|
4
|
+
from voids.core.network import Network
|
|
5
|
+
from voids.core.sample import SampleGeometry
|
|
6
|
+
from voids.core.provenance import Provenance
|
|
7
|
+
|
|
8
|
+
__all__ = ["__version__", "Network", "SampleGeometry", "Provenance"]
|
voids/_logging.py
ADDED
voids/_testing.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Internal testing utilities for voids package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_seed(seed: int) -> None:
|
|
11
|
+
"""Set deterministic seeds for the standard library and NumPy RNGs.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
seed :
|
|
16
|
+
Integer seed applied to :mod:`random` and :mod:`numpy.random`.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
The helper is intentionally narrow and does not configure any external or
|
|
21
|
+
accelerator-backed random-number generators.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
random.seed(seed)
|
|
25
|
+
np.random.seed(seed)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from voids.benchmarks.crosscheck import (
|
|
2
|
+
SinglePhaseCrosscheckSummary,
|
|
3
|
+
crosscheck_singlephase_roundtrip_openpnm_dict,
|
|
4
|
+
crosscheck_singlephase_with_openpnm,
|
|
5
|
+
)
|
|
6
|
+
from voids.benchmarks.segmented_volume import (
|
|
7
|
+
SegmentedVolumeCrosscheckResult,
|
|
8
|
+
benchmark_segmented_volume_with_openpnm,
|
|
9
|
+
)
|
|
10
|
+
from voids.benchmarks.xlb import (
|
|
11
|
+
SegmentedVolumeXLBResult,
|
|
12
|
+
XLBDirectSimulationResult,
|
|
13
|
+
XLBOptions,
|
|
14
|
+
benchmark_segmented_volume_with_xlb,
|
|
15
|
+
solve_binary_volume_with_xlb,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"SinglePhaseCrosscheckSummary",
|
|
20
|
+
"SegmentedVolumeCrosscheckResult",
|
|
21
|
+
"SegmentedVolumeXLBResult",
|
|
22
|
+
"XLBDirectSimulationResult",
|
|
23
|
+
"XLBOptions",
|
|
24
|
+
"benchmark_segmented_volume_with_openpnm",
|
|
25
|
+
"benchmark_segmented_volume_with_xlb",
|
|
26
|
+
"crosscheck_singlephase_roundtrip_openpnm_dict",
|
|
27
|
+
"crosscheck_singlephase_with_openpnm",
|
|
28
|
+
"solve_binary_volume_with_xlb",
|
|
29
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared helpers for high-level segmented-volume benchmarks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
from voids.physics.singlephase import PressureBC
|
|
8
|
+
|
|
9
|
+
DEFAULT_BENCHMARK_PRESSURE_DROP = 1.0
|
|
10
|
+
DEFAULT_BENCHMARK_REFERENCE_PRESSURE = 0.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_benchmark_pressures(
|
|
14
|
+
*,
|
|
15
|
+
delta_p: float | None = None,
|
|
16
|
+
pin: float | None = None,
|
|
17
|
+
pout: float | None = None,
|
|
18
|
+
default_delta_p: float = DEFAULT_BENCHMARK_PRESSURE_DROP,
|
|
19
|
+
default_reference_pressure: float = DEFAULT_BENCHMARK_REFERENCE_PRESSURE,
|
|
20
|
+
) -> tuple[float, float, float]:
|
|
21
|
+
"""Resolve physical pressure inputs for a high-level segmented-volume benchmark.
|
|
22
|
+
|
|
23
|
+
Notes
|
|
24
|
+
-----
|
|
25
|
+
The current high-level segmented-volume benchmarks are formulated for
|
|
26
|
+
incompressible permeability estimation. In that setting, the physically
|
|
27
|
+
relevant input is the imposed pressure drop ``delta_p = pin - pout``.
|
|
28
|
+
Absolute pressure offsets are therefore treated as a gauge choice: they can
|
|
29
|
+
be accepted for clarity or provenance, but they do not alter the current
|
|
30
|
+
permeability estimate as long as ``delta_p`` is unchanged.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
delta_p_input = None if delta_p is None else float(delta_p)
|
|
34
|
+
pin_input = None if pin is None else float(pin)
|
|
35
|
+
pout_input = None if pout is None else float(pout)
|
|
36
|
+
|
|
37
|
+
if delta_p_input is None and pin_input is None and pout_input is None:
|
|
38
|
+
delta_p_input = float(default_delta_p)
|
|
39
|
+
pout_input = float(default_reference_pressure)
|
|
40
|
+
pin_input = pout_input + delta_p_input
|
|
41
|
+
elif delta_p_input is None:
|
|
42
|
+
if pin_input is None or pout_input is None:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Provide either `delta_p`, or both `pin` and `pout`, for a "
|
|
45
|
+
"high-level segmented-volume benchmark"
|
|
46
|
+
)
|
|
47
|
+
delta_p_input = pin_input - pout_input
|
|
48
|
+
else:
|
|
49
|
+
if pin_input is None and pout_input is None:
|
|
50
|
+
pout_input = float(default_reference_pressure)
|
|
51
|
+
pin_input = pout_input + delta_p_input
|
|
52
|
+
elif pin_input is None:
|
|
53
|
+
assert pout_input is not None
|
|
54
|
+
pin_input = pout_input + delta_p_input
|
|
55
|
+
elif pout_input is None:
|
|
56
|
+
pout_input = pin_input - delta_p_input
|
|
57
|
+
else:
|
|
58
|
+
if not math.isclose(
|
|
59
|
+
pin_input - pout_input,
|
|
60
|
+
delta_p_input,
|
|
61
|
+
rel_tol=1.0e-12,
|
|
62
|
+
abs_tol=1.0e-12,
|
|
63
|
+
):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Inconsistent pressure inputs: `delta_p` must equal `pin - pout` "
|
|
66
|
+
"when all three are provided"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert delta_p_input is not None
|
|
70
|
+
assert pin_input is not None
|
|
71
|
+
assert pout_input is not None
|
|
72
|
+
|
|
73
|
+
if not all(math.isfinite(value) for value in (delta_p_input, pin_input, pout_input)):
|
|
74
|
+
raise ValueError("Benchmark pressure inputs must be finite")
|
|
75
|
+
if delta_p_input <= 0.0 or pin_input <= pout_input:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"High-level segmented-volume benchmarks require a positive physical "
|
|
78
|
+
"pressure drop (`delta_p > 0`, `pin > pout`)"
|
|
79
|
+
)
|
|
80
|
+
return pin_input, pout_input, delta_p_input
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def make_benchmark_pressure_bc(axis: str, *, pin: float, pout: float) -> PressureBC:
|
|
84
|
+
"""Return the standard inlet/outlet pressure BC for a benchmark axis."""
|
|
85
|
+
|
|
86
|
+
pin_float, pout_float, _ = resolve_benchmark_pressures(pin=pin, pout=pout)
|
|
87
|
+
return PressureBC(
|
|
88
|
+
f"inlet_{axis}min",
|
|
89
|
+
f"outlet_{axis}max",
|
|
90
|
+
pin=pin_float,
|
|
91
|
+
pout=pout_float,
|
|
92
|
+
)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from voids.core.network import Network
|
|
9
|
+
from voids.io.openpnm import to_openpnm_dict, to_openpnm_network
|
|
10
|
+
from voids.io.porespy import from_porespy
|
|
11
|
+
from voids.physics.singlephase import (
|
|
12
|
+
FluidSinglePhase,
|
|
13
|
+
PressureBC,
|
|
14
|
+
SinglePhaseOptions,
|
|
15
|
+
SinglePhaseResult,
|
|
16
|
+
solve,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class SinglePhaseCrosscheckSummary:
|
|
22
|
+
"""Summary of a solver-to-reference comparison.
|
|
23
|
+
|
|
24
|
+
Attributes
|
|
25
|
+
----------
|
|
26
|
+
reference :
|
|
27
|
+
Name of the reference implementation or workflow.
|
|
28
|
+
axis :
|
|
29
|
+
Flow axis used in the comparison.
|
|
30
|
+
permeability_abs_diff, permeability_rel_diff :
|
|
31
|
+
Absolute and relative differences between apparent permeabilities.
|
|
32
|
+
total_flow_abs_diff, total_flow_rel_diff :
|
|
33
|
+
Absolute and relative differences between total flow rates.
|
|
34
|
+
details :
|
|
35
|
+
Auxiliary metadata useful for debugging and reporting.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
reference: str
|
|
39
|
+
axis: str
|
|
40
|
+
permeability_abs_diff: float
|
|
41
|
+
permeability_rel_diff: float
|
|
42
|
+
total_flow_abs_diff: float
|
|
43
|
+
total_flow_rel_diff: float
|
|
44
|
+
details: dict[str, Any]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _rel_diff(a: float, b: float) -> float:
|
|
48
|
+
"""Compute a symmetric relative difference.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
a, b :
|
|
53
|
+
Values to compare.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
float
|
|
58
|
+
Relative difference ``abs(a - b) / max(abs(a), abs(b), 1e-30)``.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
denom = max(abs(a), abs(b), 1e-30)
|
|
62
|
+
return abs(a - b) / denom
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _summary_from_values(
|
|
66
|
+
*,
|
|
67
|
+
reference: str,
|
|
68
|
+
axis: str,
|
|
69
|
+
k_voids: float,
|
|
70
|
+
k_ref: float,
|
|
71
|
+
q_voids: float,
|
|
72
|
+
q_ref: float,
|
|
73
|
+
details: dict[str, Any],
|
|
74
|
+
) -> SinglePhaseCrosscheckSummary:
|
|
75
|
+
"""Build a crosscheck summary from scalar transport metrics.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
reference :
|
|
80
|
+
Name of the reference implementation.
|
|
81
|
+
axis :
|
|
82
|
+
Flow axis.
|
|
83
|
+
k_voids, k_ref :
|
|
84
|
+
Apparent permeabilities from ``voids`` and the reference.
|
|
85
|
+
q_voids, q_ref :
|
|
86
|
+
Total flow rates from ``voids`` and the reference.
|
|
87
|
+
details :
|
|
88
|
+
Auxiliary metadata to attach to the summary.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
SinglePhaseCrosscheckSummary
|
|
93
|
+
Comparison summary.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
return SinglePhaseCrosscheckSummary(
|
|
97
|
+
reference=reference,
|
|
98
|
+
axis=axis,
|
|
99
|
+
permeability_abs_diff=abs(k_voids - k_ref),
|
|
100
|
+
permeability_rel_diff=_rel_diff(k_voids, k_ref),
|
|
101
|
+
total_flow_abs_diff=abs(q_voids - q_ref),
|
|
102
|
+
total_flow_rel_diff=_rel_diff(q_voids, q_ref),
|
|
103
|
+
details={"k_voids": k_voids, "k_ref": k_ref, "Q_voids": q_voids, "Q_ref": q_ref, **details},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _summary_from_results(
|
|
108
|
+
reference: str, axis: str, r0: SinglePhaseResult, r1: SinglePhaseResult
|
|
109
|
+
) -> SinglePhaseCrosscheckSummary:
|
|
110
|
+
"""Build a summary from two single-phase solver results.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
reference :
|
|
115
|
+
Name of the reference workflow.
|
|
116
|
+
axis :
|
|
117
|
+
Flow axis used for extracting permeability.
|
|
118
|
+
r0, r1 :
|
|
119
|
+
Solver results to compare.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
SinglePhaseCrosscheckSummary
|
|
124
|
+
Comparison summary.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
k0 = float((r0.permeability or {}).get(axis, np.nan))
|
|
128
|
+
k1 = float((r1.permeability or {}).get(axis, np.nan))
|
|
129
|
+
q0 = float(r0.total_flow_rate)
|
|
130
|
+
q1 = float(r1.total_flow_rate)
|
|
131
|
+
return _summary_from_values(
|
|
132
|
+
reference=reference, axis=axis, k_voids=k0, k_ref=k1, q_voids=q0, q_ref=q1, details={}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def crosscheck_singlephase_roundtrip_openpnm_dict(
|
|
137
|
+
net: Network,
|
|
138
|
+
fluid: FluidSinglePhase,
|
|
139
|
+
bc: PressureBC,
|
|
140
|
+
*,
|
|
141
|
+
axis: str,
|
|
142
|
+
options: SinglePhaseOptions | None = None,
|
|
143
|
+
) -> SinglePhaseCrosscheckSummary:
|
|
144
|
+
"""Cross-check ``voids`` after a dict roundtrip through OpenPNM-style keys.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
net :
|
|
149
|
+
Network to solve and round-trip.
|
|
150
|
+
fluid :
|
|
151
|
+
Fluid properties.
|
|
152
|
+
bc :
|
|
153
|
+
Pressure boundary conditions.
|
|
154
|
+
axis :
|
|
155
|
+
Flow axis used in the permeability calculation.
|
|
156
|
+
options :
|
|
157
|
+
Optional solver configuration.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
SinglePhaseCrosscheckSummary
|
|
162
|
+
Comparison between the original ``voids`` solve and the round-tripped solve.
|
|
163
|
+
|
|
164
|
+
Notes
|
|
165
|
+
-----
|
|
166
|
+
This path does not require OpenPNM itself. It checks whether exporting to the
|
|
167
|
+
flat OpenPNM/PoreSpy naming convention and importing back into ``voids`` changes
|
|
168
|
+
any transport-relevant fields.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
options = options or SinglePhaseOptions()
|
|
172
|
+
r0 = solve(net, fluid=fluid, bc=bc, axis=axis, options=options)
|
|
173
|
+
op_dict = to_openpnm_dict(net)
|
|
174
|
+
net_rt = from_porespy(op_dict, sample=net.sample, provenance=net.provenance)
|
|
175
|
+
r1 = solve(net_rt, fluid=fluid, bc=bc, axis=axis, options=options)
|
|
176
|
+
return _summary_from_results("openpnm_dict_roundtrip", axis, r0, r1)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _openpnm_phase_factory(op, pn):
|
|
180
|
+
"""Construct a compatible OpenPNM phase object.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
op :
|
|
185
|
+
Imported OpenPNM module.
|
|
186
|
+
pn :
|
|
187
|
+
OpenPNM network object.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
Any
|
|
192
|
+
Phase object compatible with the installed OpenPNM version.
|
|
193
|
+
|
|
194
|
+
Raises
|
|
195
|
+
------
|
|
196
|
+
RuntimeError
|
|
197
|
+
If no known phase constructor works.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
for factory in (
|
|
201
|
+
lambda: op.phase.Phase(network=pn),
|
|
202
|
+
lambda: op.phases.GenericPhase(network=pn),
|
|
203
|
+
):
|
|
204
|
+
try:
|
|
205
|
+
return factory()
|
|
206
|
+
except Exception:
|
|
207
|
+
continue
|
|
208
|
+
raise RuntimeError("Unable to construct OpenPNM phase object")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _get_openpnm_pressure(sf):
|
|
212
|
+
"""Extract pore pressure from an OpenPNM StokesFlow result.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
sf :
|
|
217
|
+
OpenPNM StokesFlow algorithm object.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
numpy.ndarray
|
|
222
|
+
One-dimensional pore-pressure array.
|
|
223
|
+
|
|
224
|
+
Raises
|
|
225
|
+
------
|
|
226
|
+
RuntimeError
|
|
227
|
+
If pressure cannot be retrieved from the current OpenPNM API.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
for getter in (
|
|
231
|
+
lambda: sf["pore.pressure"],
|
|
232
|
+
lambda: sf.soln["pore.pressure"],
|
|
233
|
+
):
|
|
234
|
+
try:
|
|
235
|
+
arr = np.asarray(getter(), dtype=float)
|
|
236
|
+
if arr.ndim == 1:
|
|
237
|
+
return arr
|
|
238
|
+
except Exception:
|
|
239
|
+
continue
|
|
240
|
+
raise RuntimeError("Unable to extract pore pressures from OpenPNM StokesFlow result")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def crosscheck_singlephase_with_openpnm(
|
|
244
|
+
net: Network,
|
|
245
|
+
fluid: FluidSinglePhase,
|
|
246
|
+
bc: PressureBC,
|
|
247
|
+
*,
|
|
248
|
+
axis: str,
|
|
249
|
+
options: SinglePhaseOptions | None = None,
|
|
250
|
+
) -> SinglePhaseCrosscheckSummary:
|
|
251
|
+
"""Cross-check ``voids`` against OpenPNM StokesFlow.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
net :
|
|
256
|
+
Network to simulate.
|
|
257
|
+
fluid :
|
|
258
|
+
Fluid properties.
|
|
259
|
+
bc :
|
|
260
|
+
Pressure boundary conditions.
|
|
261
|
+
axis :
|
|
262
|
+
Flow axis used for apparent permeability.
|
|
263
|
+
options :
|
|
264
|
+
Optional solver configuration.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
SinglePhaseCrosscheckSummary
|
|
269
|
+
Comparison between ``voids`` and OpenPNM.
|
|
270
|
+
|
|
271
|
+
Raises
|
|
272
|
+
------
|
|
273
|
+
ImportError
|
|
274
|
+
If OpenPNM is not installed.
|
|
275
|
+
RuntimeError
|
|
276
|
+
If the installed OpenPNM API is incompatible with the adapter.
|
|
277
|
+
ValueError
|
|
278
|
+
If the imposed pressure drop is zero.
|
|
279
|
+
|
|
280
|
+
Notes
|
|
281
|
+
-----
|
|
282
|
+
The comparison injects the ``voids``-computed ``throat.hydraulic_conductance``
|
|
283
|
+
into OpenPNM. That means the crosscheck isolates differences in system assembly,
|
|
284
|
+
boundary-condition handling, sign conventions, and linear-solver behavior,
|
|
285
|
+
rather than differences in geometric conductance modeling.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
import openpnm as op
|
|
290
|
+
except Exception as exc: # pragma: no cover - depends on optional env
|
|
291
|
+
raise ImportError(
|
|
292
|
+
"OpenPNM is not installed. Use the 'test' pixi environment or install openpnm."
|
|
293
|
+
) from exc
|
|
294
|
+
|
|
295
|
+
options = options or SinglePhaseOptions()
|
|
296
|
+
r_voids = solve(net, fluid=fluid, bc=bc, axis=axis, options=options)
|
|
297
|
+
g = np.asarray(r_voids.throat_conductance, dtype=float)
|
|
298
|
+
|
|
299
|
+
pn = to_openpnm_network(net, copy_properties=False, copy_labels=True)
|
|
300
|
+
phase = _openpnm_phase_factory(op, pn)
|
|
301
|
+
phase["throat.hydraulic_conductance"] = g
|
|
302
|
+
|
|
303
|
+
sf = op.algorithms.StokesFlow(network=pn, phase=phase)
|
|
304
|
+
inlet_mask = np.asarray(net.pore_labels[bc.inlet_label], dtype=bool)
|
|
305
|
+
outlet_mask = np.asarray(net.pore_labels[bc.outlet_label], dtype=bool)
|
|
306
|
+
inlet = np.where(inlet_mask)[0]
|
|
307
|
+
outlet = np.where(outlet_mask)[0]
|
|
308
|
+
|
|
309
|
+
if hasattr(sf, "set_value_BC"):
|
|
310
|
+
sf.set_value_BC(pores=inlet, values=float(bc.pin))
|
|
311
|
+
sf.set_value_BC(pores=outlet, values=float(bc.pout))
|
|
312
|
+
elif hasattr(sf, "set_BC"):
|
|
313
|
+
sf.set_BC(pores=inlet, bctype="value", bcvalues=float(bc.pin))
|
|
314
|
+
sf.set_BC(pores=outlet, bctype="value", bcvalues=float(bc.pout))
|
|
315
|
+
else: # pragma: no cover
|
|
316
|
+
raise RuntimeError("OpenPNM StokesFlow object does not expose a recognizable BC API")
|
|
317
|
+
|
|
318
|
+
sf.run()
|
|
319
|
+
p_ref = _get_openpnm_pressure(sf)
|
|
320
|
+
|
|
321
|
+
q_rate = np.asarray(sf.rate(pores=inlet), dtype=float)
|
|
322
|
+
q_ref_raw = float(q_rate.sum())
|
|
323
|
+
q_ref = q_ref_raw
|
|
324
|
+
if np.isfinite(q_ref) and np.isfinite(r_voids.total_flow_rate):
|
|
325
|
+
if np.isclose(abs(q_ref), abs(r_voids.total_flow_rate), rtol=1e-8, atol=1e-14):
|
|
326
|
+
q_ref = float(np.copysign(abs(q_ref), r_voids.total_flow_rate))
|
|
327
|
+
|
|
328
|
+
dP = float(bc.pin - bc.pout)
|
|
329
|
+
if abs(dP) == 0.0:
|
|
330
|
+
raise ValueError("Pressure drop pin-pout must be nonzero")
|
|
331
|
+
L = net.sample.length_for_axis(axis)
|
|
332
|
+
Axs = net.sample.area_for_axis(axis)
|
|
333
|
+
k_ref = abs(q_ref_raw) * fluid.viscosity * L / (Axs * abs(dP))
|
|
334
|
+
k_voids = float((r_voids.permeability or {}).get(axis, np.nan))
|
|
335
|
+
|
|
336
|
+
return _summary_from_values(
|
|
337
|
+
reference="openpnm_stokesflow",
|
|
338
|
+
axis=axis,
|
|
339
|
+
k_voids=k_voids,
|
|
340
|
+
k_ref=float(k_ref),
|
|
341
|
+
q_voids=float(r_voids.total_flow_rate),
|
|
342
|
+
q_ref=float(q_ref),
|
|
343
|
+
details={
|
|
344
|
+
"openpnm_version": getattr(op, "__version__", "unknown"),
|
|
345
|
+
"q_ref_raw": q_ref_raw,
|
|
346
|
+
"n_inlet_pores": int(inlet.size),
|
|
347
|
+
"n_outlet_pores": int(outlet.size),
|
|
348
|
+
"conductance_model": options.conductance_model,
|
|
349
|
+
"solver_voids": options.solver,
|
|
350
|
+
"p_ref_min": float(np.min(p_ref)) if p_ref.size else np.nan,
|
|
351
|
+
"p_ref_max": float(np.max(p_ref)) if p_ref.size else np.nan,
|
|
352
|
+
},
|
|
353
|
+
)
|