Skip to content

Commit

Permalink
Improved emulsion support
Browse files Browse the repository at this point in the history
* Added more flexibility in specifying dtype
* Added constructor for empty emulsions
* Removed grid attribute for emulsions determined via image analysis
  • Loading branch information
david-zwicker committed Aug 30, 2023
1 parent 9f77d53 commit 4f3fee7
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 57 deletions.
36 changes: 32 additions & 4 deletions droplets/emulsions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def __init__(
droplets: Optional[Iterable[SphericalDroplet]] = None,
*,
copy: bool = True,
dtype: np.typing.DTypeLike = None,
dtype: Union[np.typing.DTypeLike, np.ndarray, SphericalDroplet] = None,
force_consistency: bool = False,
grid: Optional[GridBase] = None,
):
Expand All @@ -76,7 +76,8 @@ def __init__(
Whether to make a copy of the droplet or not
dtype (:class:`~numpy.tpying.DTypeLike`):
The dtype describing what droplets are stored in the emulsion. Providing
this is usually only necessary for creating empty emulsions.
this is usually only necessary for creating empty emulsions. Instead of
a dtype, an array or an example droplet can also be supplied.
force_consistency (bool, optional):
Whether to ensure that all droplets are of the same type, i.e., their
data is described by the same dtype and matches `dtype` if given.
Expand All @@ -85,15 +86,39 @@ def __init__(

if grid is not None:
# deprecated on 2023-08-29
raise RuntimeError
warnings.warn("`grid` argument is deprecated", DeprecationWarning)

# store general information about droplets
self.dtype = dtype
# store general information about droplets using a single dtype
if isinstance(dtype, SphericalDroplet):
dtype = dtype.data.dtype # extract dtype from actual droplet
elif isinstance(dtype, np.ndarray):
dtype = dtype.dtype # extract dtype from numpy array
elif dtype is not None:
dtype = np.dtype(dtype) # assume a proper dtype is given
assert dtype is None or isinstance(dtype, np.dtype)
if isinstance(dtype, np.record):
self.dtype: np.typing.DTypeLike = dtype.dtype # strip record part
else:
self.dtype = dtype

# add the actual droplets that are specified
if droplets is not None:
self.extend(droplets, copy=copy, force_consistency=force_consistency)

@classmethod
def empty(cls, droplet: SphericalDroplet) -> Emulsion:
"""create empty emulsion with particular droplet type
Args:
droplet (:class:`~droplets.droplets.SphericalDroplet`):
An example for a droplet, which defines the type of
Returns:
:class:`Emulsion`: The empty emulsion
"""
return cls([], dtype=droplet, copy=False)

@classmethod
def from_random(
cls,
Expand Down Expand Up @@ -127,6 +152,9 @@ def from_random(
The class that is used to create droplets.
rng (:class:`~numpy.random.Generator`):
Random number generator (default: :func:`~numpy.random.default_rng()`)
Returns:
:class:`Emulsion`: The emulsion containing the random droplets
"""
if rng is None:
rng = np.random.default_rng()
Expand Down
27 changes: 15 additions & 12 deletions droplets/image_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@
from numpy.fft import fftn as np_fftn

from pde.fields import ScalarField
from pde.grids import CylindricalSymGrid
from pde.grids import CartesianGrid, CylindricalSymGrid
from pde.grids.base import GridBase
from pde.grids.cartesian import CartesianGrid
from pde.grids.spherical import SphericalSymGridBase
from pde.tools.math import SmoothData1D
from pde.tools.typing import NumberOrArray
Expand Down Expand Up @@ -113,7 +112,8 @@ def _locate_droplets_in_mask_cartesian(
# locate individual clusters in the padded image
labels, num_labels = ndimage.label(mask_padded)
if num_labels == 0:
return Emulsion([], grid=grid)
example_drop = SphericalDroplet(np.zeros(grid.dim), radius=0)
return Emulsion.empty(example_drop)
indices = range(1, num_labels + 1)

# create and emulsion from this of droplets
Expand All @@ -136,12 +136,12 @@ def _locate_droplets_in_mask_cartesian(
)

# filter overlapping droplets (e.g. due to duplicates)
emulsion = Emulsion(droplets, grid=grid)
emulsion = Emulsion(droplets)
num_candidates = len(emulsion)
if num_candidates < num_labels:
grid._logger.info(f"Only {num_candidates} candidate(s) inside bounds")

emulsion.remove_overlapping()
emulsion.remove_overlapping(grid=grid)
if len(emulsion) < num_candidates:
grid._logger.info(f"Only {num_candidates} candidate(s) not overlapping")

Expand All @@ -165,7 +165,8 @@ def _locate_droplets_in_mask_spherical(
# locate clusters in the binary image
labels, num_labels = ndimage.label(mask)
if num_labels == 0:
return Emulsion([], grid=grid)
example_drop = SphericalDroplet(np.zeros(grid.dim), radius=0)
return Emulsion.empty(example_drop)

# locate clusters around origin
object_slices = ndimage.find_objects(labels)
Expand All @@ -180,9 +181,10 @@ def _locate_droplets_in_mask_spherical(

# return an emulsion of droplets
if droplet:
return Emulsion([droplet], grid=grid)
return Emulsion([droplet])
else:
return Emulsion([], grid=grid)
example_drop = SphericalDroplet(np.zeros(grid.dim), radius=0)
return Emulsion.empty(example_drop)


def _locate_droplets_in_mask_cylindrical_single(
Expand All @@ -200,7 +202,8 @@ def _locate_droplets_in_mask_cylindrical_single(
# locate the individual clusters
labels, num_features = ndimage.label(mask)
if num_features == 0:
return Emulsion([], grid=grid)
example_drop = SphericalDroplet(np.zeros(grid.dim), radius=0)
return Emulsion.empty(example_drop)

# locate clusters on the symmetry axis
object_slices = ndimage.find_objects(labels)
Expand Down Expand Up @@ -230,7 +233,7 @@ def _locate_droplets_in_mask_cylindrical_single(
SphericalDroplet.from_volume(np.array([0, 0, p[2]]), v)
for p, v in zip(pos, vol)
)
return Emulsion(droplets, grid=grid)
return Emulsion(droplets)


def _locate_droplets_in_mask_cylindrical(
Expand Down Expand Up @@ -263,7 +266,7 @@ def _locate_droplets_in_mask_cylindrical(
grid._logger.info(f"Found {len(candidates)} droplet candidates.")

# keep droplets that are inside the central area
droplets = Emulsion(grid=grid)
droplets = Emulsion()
for droplet in candidates:
# correct for the additional padding of the array
droplet.position[2] -= grid.length
Expand Down Expand Up @@ -432,7 +435,7 @@ def locate_droplets(
droplets.append(droplet)

# return droplets as an emulsion
emulsion = Emulsion(droplets, grid=phase_field.grid)
emulsion = Emulsion(droplets)
if minimal_radius > -np.inf:
emulsion.remove_small(minimal_radius)
return emulsion
Expand Down
91 changes: 50 additions & 41 deletions tests/test_emulsion.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
from droplets import DiffuseDroplet, Emulsion, SphericalDroplet, droplets, emulsions


def test_empty_emulsion():
def test_empty_emulsion(caplog):
"""test an emulsions without any droplets"""
e = Emulsion([], grid=UnitGrid([2]))
caplog.set_level(logging.WARNING)

e = Emulsion([])
assert not e
assert len(e) == 0
assert e == e.copy()
Expand All @@ -36,10 +38,47 @@ def test_empty_emulsion():
for b in [True, False]:
np.testing.assert_array_equal(e.get_neighbor_distances(b), np.array([]))

# define droplets
d1 = SphericalDroplet([1], 2)
d2 = DiffuseDroplet([1], 2, 1)

e = Emulsion([])
assert len(e) == 0
with pytest.raises(RuntimeError):
e.data
e.append(d1, force_consistency=True)
assert e.dtype == d1.data.dtype

e = Emulsion.empty(d1)
assert len(e) == 0
assert e.data.size == 0
e.append(d1, force_consistency=True)
assert e.dtype == d1.data.dtype

for dt in [d1.data.dtype, d1.data, d1]:
e = Emulsion([], dtype=dt)
assert len(e) == 0
assert e.data.size == 0
assert e.data.ndim == 1
assert e.dtype == e.data.dtype
assert e.dtype == d1.data.dtype
e.append(d1, force_consistency=True)

e = Emulsion([], dtype=d1.data.dtype)
with pytest.raises(ValueError):
e.append(d2, force_consistency=True)

e = Emulsion([], dtype=d1.data.dtype)
e.append(d2)
assert len(e) == 1

e.data # raises warning
assert "inconsistent" in caplog.text


def test_emulsion_single():
"""test an emulsions with a single droplet"""
e = Emulsion([], grid=UnitGrid([2]))
e = Emulsion([])
e.append(DiffuseDroplet([10], 3, 1))
assert e
assert len(e) == 1
Expand All @@ -63,9 +102,8 @@ def test_emulsion_single():

def test_emulsion_two():
"""test an emulsions with two droplets"""
grid = UnitGrid([30])
e = Emulsion([DiffuseDroplet([10], 3, 1)], grid=grid)
e1 = Emulsion([DiffuseDroplet([20], 5, 1)], grid=grid)
e = Emulsion([DiffuseDroplet([10], 3, 1)])
e1 = Emulsion([DiffuseDroplet([20], 5, 1)])
e.extend(e1)
assert e
assert len(e) == 2
Expand All @@ -89,39 +127,6 @@ def test_emulsion_two():
np.testing.assert_array_equal(e.get_neighbor_distances(True), np.array([2, 2]))


def test_emulsion_emtpy(caplog):
"""test incompatible droplets in an emulsion"""
caplog.set_level(logging.WARNING)

# define droplets
d1 = SphericalDroplet([1], 2)
d2 = DiffuseDroplet([1], 2, 1)

e = Emulsion([])
assert len(e) == 0
with pytest.raises(RuntimeError):
e.data
e.append(d1, force_consistency=True)
assert e.dtype == d1.data.dtype

e = Emulsion([], dtype=d1.data.dtype)
assert len(e) == 0
assert e.data.size == 0
assert e.dtype == e.data.dtype == d1.data.dtype
e.append(d1, force_consistency=True)

e = Emulsion([], dtype=d1.data.dtype)
with pytest.raises(ValueError):
e.append(d2, force_consistency=True)

e = Emulsion([], dtype=d1.data.dtype)
e.append(d2)
assert len(e) == 1

e.data # raises warning
assert "inconsistent" in caplog.text


def test_emulsion_incompatible():
"""test incompatible droplets in an emulsion"""
# different type
Expand Down Expand Up @@ -162,10 +167,14 @@ def test_emulsion_io(tmp_path):
"""test writing and reading emulsions"""
path = tmp_path / "test_emulsion_io.hdf5"

drop_diff = DiffuseDroplet([0, 1], 10, 0.5)
drop_pert = droplets.PerturbedDroplet2D([0, 1], 3, 1, [1, 2, 3])

es = [
Emulsion(),
Emulsion([DiffuseDroplet([0, 1], 10, 0.5)] * 2),
Emulsion([droplets.PerturbedDroplet2D([0, 1], 3, 1, [1, 2, 3])]),
Emulsion([], dtype=drop_diff.data.dtype),
Emulsion([drop_diff] * 2),
Emulsion([drop_pert]),
]
for e1 in es:
e1.to_file(path)
Expand Down

0 comments on commit 4f3fee7

Please sign in to comment.