Skip to content

General upgrades and fixes #1075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
69052b6
using functools.partial to provide ncores to edt.edt
jgostick Apr 28, 2025
541e095
Updating zip_to_stack function to use skimage's method only since oth…
jgostick Apr 28, 2025
e775c79
Trying to fix performance problem in imbibition
jgostick May 2, 2025
ba13365
This seems to have fixed it...30 mins down to 1 min!
jgostick May 2, 2025
ad7fe29
adding docstrings to erode and dilate
jgostick May 2, 2025
25213fb
fixed bug in injection-qbip for 3D images
jgostick May 4, 2025
4e0773b
fixing wrong option name in drainage and imbibition
jgostick May 5, 2025
f31a362
renamed invasion filters file to displacement, working on filling tra…
jgostick May 5, 2025
20ca05f
altering cluster finding/filling funcs
jgostick May 5, 2025
5192162
catching last few changes
jgostick May 6, 2025
3a7edc5
Merge branch 'dev' into general_upgrades_and_fixes
jgostick May 7, 2025
bc9ac5e
cleaning up merge
jgostick May 7, 2025
4b978ee
tweaks
jgostick May 16, 2025
4e7142c
removing redundant checks from trapping sub-funcs
jgostick May 20, 2025
f63ccbe
fixing trapping w residual bug in drainage
jgostick May 20, 2025
5fddff1
fixing stash merge
jgostick May 22, 2025
3a55ca4
Fixing import of edt's jitted functions
jgostick May 27, 2025
6d0d1c6
fixing get_edt
jgostick May 27, 2025
b9187e4
changed arg from mask to mask_source
jgostick Jun 3, 2025
0ad95d9
Updating capillary transform function and docs
jgostick Jun 3, 2025
e60a0b1
Preventing residual and trapping in drainage
jgostick Jun 3, 2025
2e85523
updating tests to remove residual/trapping combes
jgostick Jun 3, 2025
6267e26
updating bond_number to use either diameter or radius
jgostick Jun 20, 2025
8cbc522
updating capillary_transform to ensure theta is between 90 and 180
jgostick Jun 20, 2025
6ba0f7d
changed use_diameter default to false
jgostick Jun 20, 2025
2dbae32
fixed bug in generation of pc range
jgostick Jun 20, 2025
5ce1183
updating uv deps to remove skfmm completely
jgostick Jun 20, 2025
955afb7
removing fmm from magnet and updating test
jgostick Jun 20, 2025
2b848ab
Merge branch 'dev' into general_upgrades_and_fixes
jgostick Jun 20, 2025
48e5dd0
fixing fmm import check in magnet
jgostick Jun 20, 2025
b80b0e0
readding pywavelets which is a silent dep for nl_means filter it seems
jgostick Jun 20, 2025
d1e0fa2
updating example after name change
jgostick Jun 20, 2025
aeb5d90
using min_size in displacement funcs
jgostick Jun 20, 2025
6e43cb1
removing min_size option from example
jgostick Jun 20, 2025
f3357e2
updating min_size removal to account for returned tuple
jgostick Jun 20, 2025
0a654e0
fixing detupling of results object, and updating some examples
jgostick Jun 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

121 changes: 33 additions & 88 deletions examples/networks/reference/magnet.ipynb

Large diffs are not rendered by default.

43 changes: 21 additions & 22 deletions examples/simulations/reference/drainage.ipynb

Large diffs are not rendered by default.

Large diffs are not rendered by default.

11 changes: 3 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ dependencies = [
"scikit-image<0.25.0",
"scipy",
"tqdm",
"pywavelets",
"setuptools",
"trimesh>=4.5.3",
"pyevtk>=1.6.0",
"matplotlib-inline>=0.1.7",
"numpy-stl>=3.2.0",
"imageio>=2.37.0",
"nanomesh>=0.9.1",
"pywavelets>=1.8.0",
]
readme = "README.md"
requires-python = ">=3.10,<3.13"
Expand All @@ -59,19 +61,12 @@ test = [
"pytest-split",
]
extras = [
"imageio",
"nanomesh",
"numpy-stl",
"pyevtk",
"scikit-fmm",
"scikit-learn",
"trimesh",
]
extras-macos = [
"imageio",
"numpy-stl",
"pyevtk",
"scikit-fmm",
"scikit-learn",
"trimesh",
]
Expand Down
4 changes: 2 additions & 2 deletions src/porespy/filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
filters.find_invalid_pores
filters.find_peaks
filters.find_surface_pores
filters.find_trapped_regions
filters.find_trapped_clusters
filters.flood
filters.flood_func
filters.hold_peaks
Expand Down Expand Up @@ -74,5 +74,5 @@
from ._size_seq_satn import *
from ._snows import *
from ._transforms import *
from ._invasion import *
from ._displacement import *
from ._morphology import *
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
flood_func,
)
from porespy.generators import (
borders,

Check failure on line 22 in src/porespy/filters/_displacement.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

src/porespy/filters/_displacement.py:22:5: F401 `porespy.generators.borders` imported but unused
)


Expand All @@ -29,15 +29,15 @@


__all__ = [
"fill_trapped_voxels",
"find_trapped_regions",
"find_small_clusters",
"find_trapped_clusters",
]


def fill_trapped_voxels(
seq: npt.NDArray,
def find_small_clusters(
im: npt.NDArray,
trapped: npt.NDArray = None,
max_size: int = 1,
min_size: int = 1,
conn: str = 'min',
):
r"""
Expand All @@ -46,15 +46,12 @@

Parameters
----------
seq : ndarray
The sequence map resulting from an invasion process where trapping has
been applied, such that trapped voxels are labelled -1.
trapped : ndarray, optional
The boolean array of the trapped voxels. If this is not available than all
voxels in `seq` with a value < 0 are used.
im : ndarray
The boolean image of the porous media with `True` indicating void.
trapped : ndarray
The boolean array of the trapped voxels.
min_size : int
The maximum size of the clusters which are to be filled. Clusters larger
than this size are left trapped.
The minimum size of the clusters which are to be filled.
conn : str
Controls the shape of the structuring element used to find neighboring
voxels when looking for sequence values to place into un-trapped voxels.
Expand All @@ -74,51 +71,113 @@
results
A dataclass-like object with the following images as attributes:

========== ==================================================================
Attribute Description
========== ==================================================================
'seq' The invasion sequence image with erroneously trapped voxels set
back to untrapped, and given the sequence number of their
nearby voxels.
'trapped' An updated mask of trapped voxels with the erroneously trapped
voxels removed (i.e. set to `False`).
========== ==================================================================

Notes
-----
This function has to essentially guess which sequence value to put into each
un-trapped voxel so the sequence values can differ between the output of
this function and the result returned by the various invasion algorithms where
the trapping is computed internally. However, the fluid configuration for a
given saturation will be nearly identical.
============= ==============================================================
Attribute Description
============= ==============================================================
`im_trapped` An updated mask of trapped voxels with the small clusters of
trapped voxels removed (i.e. set to `False`).
`im_released` An image with `True` values indicating formerly trapped voxels
which were smaller than `min_size` so set to untrapped.
============= ==============================================================

"""
if trapped is None:
trapped = seq < 0

se = strel[seq.ndim][conn].copy()
size = region_size(trapped, conn=conn)
mask = (size <= max_size)*(size > 0)
cluster_size = region_size(trapped, conn=conn)
mask = (cluster_size <= min_size)*(cluster_size > 0)

Check warning on line 85 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L84-L85

Added lines #L84 - L85 were not covered by tests
trapped[mask] = False

mx = spim.maximum_filter(seq*~trapped, footprint=se)
mx = flood_func(mx, np.amax, labels=spim.label(mask, structure=se)[0])
seq[mask] = mx[mask]

results = Results()
results.im_seq = seq
results.im_trapped = trapped
results.im_released = mask

Check warning on line 90 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L90

Added line #L90 was not covered by tests

return results


def find_trapped_regions(
def fill_trapped_clusters(
im: npt.NDArray,
trapped: npt.NDArray,
seq: npt.NDArray = None,
size: npt.NDArray = None,
pc: npt.NDArray = None,
min_size: int = 0,
conn: Literal['min', 'max'] = 'min',
mode: Literal['drainage', 'imbibition'] = 'drainage',
):
r"""

Parameters
----------
im : ndarray
The boolean image of the porous media with `True` indicating void.
trapped : ndarray
The boolean array of the trapped voxels.
seq : ndarray
The sequence map produced by a displacement algorithm. Regions labelled -1
are considered trapped, and regions labelled 0 are considered residual
invading phase.
size : ndarray
The size map produced by a displacement algorithm. Regions labelled -1
are considered trapped, and regions labelled 0 are considered solid.
pc : ndarray
The capillary pressure map produced by a displacement algorithm.
conn : str
Controls the shape of the structuring element used to find neighboring
voxels when looking for neighbor values to place into un-trapped voxels.
Options are:

========= ==================================================================
Option Description
========= ==================================================================
'min' This corresponds to a cross with 4 neighbors in 2D and 6
neighbors in 3D.
'max' This corresponds to a square or cube with 8 neighbors in 2D and
26 neighbors in 3D.
========= ==================================================================
"""
se = strel[im.ndim][conn].copy()
results = Results()

Check warning on line 137 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L136-L137

Added lines #L136 - L137 were not covered by tests

if seq is not None:
seq[trapped] = -1
seq = make_contiguous(seq, mode='symmetric')
if size is not None:
size[trapped] = -1
if pc is not None:
pc[trapped] = np.inf if mode == 'drainage' else -np.inf

Check warning on line 145 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L139-L145

Added lines #L139 - L145 were not covered by tests

if min_size > 0:
trapped, released = find_small_clusters(

Check warning on line 148 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L147-L148

Added lines #L147 - L148 were not covered by tests
im=im,
trapped=trapped,
min_size=min_size,
conn=conn,
)
labels = spim.label(released, structure=se)[0]
if seq is not None:
mx = spim.maximum_filter(seq*~released, footprint=se)
mx = flood_func(mx, np.amax, labels=labels)
seq[released] = mx[released]
results.im_seq = seq
if size is not None:
mx = spim.maximum_filter(size*~released, footprint=se)
mx = flood_func(mx, np.amax, labels=labels)
size[released] = mx[released]
results.im_size = size
if pc is not None:
tmp = pc.copy()
tmp[np.isinf(tmp)] = 0
mx = spim.maximum_filter(tmp*~released, footprint=se)
mx = flood_func(mx, np.amax, labels=labels)
pc[released] = mx[released]
results.im_pc = pc
return results

Check warning on line 172 in src/porespy/filters/_displacement.py

View check run for this annotation

Codecov / codecov/patch

src/porespy/filters/_displacement.py#L154-L172

Added lines #L154 - L172 were not covered by tests


def find_trapped_clusters(
im: npt.ArrayLike,
seq: npt.ArrayLike,
outlets: npt.ArrayLike,
return_mask: bool = True,
conn: Literal['min', 'max'] = 'min',
method: Literal['queue', 'cluster'] = 'cluster',
min_size: int = 0,
method: Literal['queue', 'labels'] = 'labels',
):
r"""
Find the trapped regions given an invasion sequence map and specified outlets
Expand All @@ -136,11 +195,6 @@
outlets : ndarray
An image the same size as ``im`` with ``True`` indicating outlets
and ``False`` elsewhere.
return_mask : bool
If ``True`` (default) then the returned image is a boolean mask
indicating which voxels are trapped. If ``False``, then a copy of
``seq`` is returned with the trapped voxels set to uninvaded (-1) and
the remaining invasion sequence values adjusted accordingly.
conn : str
Controls the shape of the structuring element used to determin if voxels
are connected. Options are:
Expand All @@ -160,7 +214,7 @@
========= ==================================================================
Option Description
========= ==================================================================
'cluster' Uses `scipy.ndimage.label` to find all clusters of invading phase
'labels' Uses `scipy.ndimage.label` to find all clusters of invading phase
connected to the outlet at each value of sequence found on the
outlet face. This method is faster if `ibop` was used for the
simulation.
Expand All @@ -169,39 +223,28 @@
`qbip` was used for the simulation.
========= ==================================================================

min_size : int
Any clusters of trapped voxels smaller than this size will be set to *not
trapped*. This is useful to prevent small voxels along edges of the void
space from being set to trapped. These can appear to be trapped due to the
jagged nature of the digital image. The default is 0, meaning this
adjustment is not applied, but a value of 3 or 4 is recommended to activate
this adjustment.

Returns
-------
trapped : ND-image
An image, the same size as ``seq``. If ``return_mask`` is ``True``,
then the image has ``True`` values indicating the trapped voxels. If
``return_mask`` is ``False``, then a copy of ``seq`` is returned with
trapped voxels set to -1.
trapped : ndarray
A boolean mask indicating which voxels were found to be trapped.

Examples
--------
`Click here
<https://porespy.org/examples/filters/reference/find_trapped_regions.html>`_
<https://porespy.org/examples/filters/reference/find_trapped_clusters.html>`_
to view online example.

"""
if method == 'queue':
seq = np.copy(seq) # Need a copy since the queue method updates 'in-place'
seq_temp = _find_trapped_regions_queue(
seq_temp = _find_trapped_clusters_queue(
im=im,
seq=seq,
outlets=outlets,
conn=conn,
)
elif method == 'cluster':
seq_temp = _find_trapped_regions_cluster(
elif method == 'labels':
seq_temp = _find_trapped_clusters_labels(
im=im,
seq=seq,
outlets=outlets,
Expand All @@ -210,17 +253,10 @@
else:
raise Exception(f'{method} is not a supported method')

if min_size > 0: # Fix pixels on solid surfaces
seq_temp, trapped = fill_trapped_voxels(seq_temp, max_size=min_size)
else:
trapped = (seq_temp == -1)*im
if return_mask:
return trapped
else:
return seq_temp
return (seq_temp == -1)*im


def _find_trapped_regions_cluster(
def _find_trapped_clusters_labels(
im: npt.ArrayLike,
seq: npt.ArrayLike,
outlets: npt.ArrayLike,
Expand All @@ -229,11 +265,7 @@
r"""
This version is meant for IBOP (i.e. drainage or MIO) simulations
"""
if im is None:
im = ~(seq == 0)
seq = np.copy(seq)
if outlets is None:
outlets = borders(seq.shape, mode='faces')
non_perc = find_disconnected_voxels(im, surface=True)
se = strel[im.ndim][conn].copy()
mask = seq < 0 # This is used again at the end of the function to fix seq
Expand All @@ -243,7 +275,6 @@
tmp = seq*mask_dil
new_seq = flood(im=tmp, labels=spim.label(mask_dil)[0], mode='maximum')
seq = seq*~mask + new_seq*mask
# TODO: Convert outlets to indices instead of mask to save time (maybe?)
outlets = np.where(outlets)
# Remove all trivially trapped regions (i.e. invaded after last outlet)
trapped = np.zeros_like(seq, dtype=bool)
Expand All @@ -270,7 +301,7 @@
return seq


def _find_trapped_regions_queue(
def _find_trapped_clusters_queue(
im: npt.NDArray,
seq: npt.NDArray,
outlets: npt.NDArray,
Expand Down
Loading
Loading