Skip to content
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

handling mixed slices and volumes #62

Merged
merged 9 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,17 @@ jobs:
# setup-miniconda: https://github.com/conda-incubator/setup-miniconda
# and
# tox-conda: https://github.com/tox-dev/tox-conda
- name: Install dependencies
- name: Upgrade pip
run: |
python -m pip install --upgrade pip

- name: Limit pip for windows
if: runner.os == 'Windows'
run: |
python -m pip install --upgrade "pip<22"

- name: Install dependencies
run: |
python -m pip install setuptools tox tox-gh-actions

# this runs the platform-specific tests declared in tox.ini
Expand Down
65 changes: 61 additions & 4 deletions src/yt_napari/_model_ingestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ def _le_re_to_cen_wid(

class LayerDomain:
# container for domain info for a single layer
# left_edge, right_edge, resolution, n_d are all self explanatory.
# other parameters:
#
# new_dim_value: optional unyt_quantity.
# If n_d == 2, and upgrade_to_3D is subsequently called, then this value
# will be used for the new
# new_dim_axis: optional int.
# the index position to add the new_dim_position, default is last
def __init__(
self,
left_edge: unyt_array,
right_edge: unyt_array,
resolution: tuple,
n_d: int = 3,
n_d: Optional[int] = 3,
new_dim_value: Optional[unyt_quantity] = None,
new_dim_axis: Optional[int] = 2,
):

if len(left_edge) != len(right_edge):
Expand All @@ -33,7 +43,10 @@ def __init__(
if len(resolution) == 1:
resolution = resolution * n_d # assume same in every dim
else:
raise ValueError("length of resolution does not match edge arrays")
msg = f"{len(resolution)}:{len(left_edge)}"
raise ValueError(
f"length of resolution does not match edge arrays {msg}"
)

self.left_edge = left_edge
self.right_edge = right_edge
Expand All @@ -43,6 +56,36 @@ def __init__(
self.aspect_ratio = self.width / self.width[0]
self.requires_scale = np.any(self.aspect_ratio != unyt_array(1.0, ""))
self.n_d = n_d
if new_dim_value is None:
new_dim_value = unyt_quantity(0.0, left_edge.units)
self.new_dim_value = new_dim_value
self.new_dim_axis = new_dim_axis

def upgrade_to_3D(self):
# note: this is not (yet) used when loading planes in 3d scenes.
if self.n_d == 3:
return # already 3D, nothing to do

if self.n_d == 2:
new_l_r = self.new_dim_value
axid = self.new_dim_axis
self.left_edge = _insert_to_unyt_array(self.left_edge, new_l_r, axid)
self.right_edge = _insert_to_unyt_array(self.right_edge, new_l_r, axid)
self.resolution = _insert_to_unyt_array(self.right_edge, 1, axid)
self.grid_width = _insert_to_unyt_array(self.grid_width, 0, axid)
self.aspect_ratio = _insert_to_unyt_array(self.aspect_ratio, 1.0, axid)
self.n_d = 3


def _insert_to_unyt_array(
x: unyt_array, new_value: Union[float, unyt_array], position: int
) -> unyt_array:
# just for scalars
if isinstance(new_value, unyt_array):
# reminder: unyt_quantity is instance of unyt_array
new_value = new_value.to(x.units).d

return unyt_array(np.insert(x.d, position, new_value), x.units)


# define types for the napari layer tuples
Expand All @@ -65,6 +108,9 @@ def __init__(self, ref_layer_domain: LayerDomain):
self.grid_width = ref_layer_domain.grid_width
self.aspect_ratio = ref_layer_domain.aspect_ratio

# and store the full domain
self.layer_domain = ref_layer_domain

def calculate_scale(self, other_layer: LayerDomain) -> unyt_array:
# calculate the pixel scale for a layer relative to the reference

Expand All @@ -73,6 +119,7 @@ def calculate_scale(self, other_layer: LayerDomain) -> unyt_array:
# layers. scale > 1 will take a small number of pixels and stretch them
# to cover more pixels. scale < 1 will shrink them.
sc = other_layer.grid_width / self.grid_width
sc[sc == 0] = 1.0

# we also need to multiply by the initial reference layer aspect ratio
# to account for any initial distortion.
Expand All @@ -96,6 +143,12 @@ def align_sanitize_layer(self, layer: SpatialLayer) -> Layer:
# pull out the elements of the SpatialLayer tuple
im_arr, im_kwargs, layer_type, domain = layer

# bypass if adding a 2d layer
if domain.n_d == 2 and self.layer_domain.n_d == 3:
# when mixing 2d and 3d selections, cannot guarantee alignment
# or scaling, simply return with no adjustment
return (im_arr, im_kwargs, layer_type)

# calculate scale and translation
scale = self.calculate_scale(domain)
translate = self.calculate_translation(domain)
Expand Down Expand Up @@ -371,7 +424,12 @@ def _process_slice(
)

layer_domain = LayerDomain(
left_edge=LE, right_edge=RE, resolution=resolution, n_d=2
left_edge=LE,
right_edge=RE,
resolution=resolution,
n_d=2,
new_dim_axis=2,
new_dim_value=0.0,
)

return frb, layer_domain
Expand Down Expand Up @@ -428,7 +486,6 @@ def _process_validated_model(model: InputModel) -> List[SpatialLayer]:
# return a list of layer tuples with domain information

layer_list = []

# our model is already validated, so we can assume the field exist with
# their correct types. This is all the yt-specific code required to load a
# dataset and return a plain numpy array
Expand Down
80 changes: 78 additions & 2 deletions src/yt_napari/_tests/test_model_ingestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,72 @@ def test_layer_domain(domains_to_test):
assert np.all(layer_domain.width == d.width)

# check some instantiation things
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="length of edge arrays must match"):
_ = _mi.LayerDomain(d.left_edge, unyt.unyt_array([1, 2], "m"), d.resolution)

with pytest.raises(ValueError):
with pytest.raises(ValueError, match="length of resolution does not"):
_ = _mi.LayerDomain(d.left_edge, d.right_edge, (10, 12))

ld = _mi.LayerDomain(d.left_edge, d.right_edge, (10,))
assert len(ld.resolution) == 3


def test_layer_domain_dimensionality():
# note: the code being tested here could be used to help orient the slices
# in 3D but is not currently used.
# sets of left_edge, right_edge, center, width, res
le = unyt.unyt_array([1.0, 1.0], "km")
re = unyt.unyt_array([2000.0, 2000.0], "m")
res = (10, 20)
ld = _mi.LayerDomain(le, re, res, n_d=2)
assert ld.n_d == 2

ld.upgrade_to_3D()
assert ld.n_d == 3
assert len(ld.left_edge) == 3
assert ld.left_edge[-1] == 0.0
ld.upgrade_to_3D() # nothing should happen

ld = _mi.LayerDomain(le, re, res, n_d=2, new_dim_value=0.5)
ld.upgrade_to_3D()
assert ld.left_edge[2] == unyt.unyt_quantity(0.5, le.units)

new_val = unyt.unyt_quantity(0.5, "km")
ld = _mi.LayerDomain(le, re, res, n_d=2, new_dim_value=new_val)
ld.upgrade_to_3D()
assert ld.left_edge[2].to("km") == new_val

ld = _mi.LayerDomain(le, re, res, n_d=2, new_dim_value=new_val, new_dim_axis=0)
ld.upgrade_to_3D()
assert ld.left_edge[0].to("km") == new_val


_test_cases_insert = [
(
unyt.unyt_array([1.0, 1.0], "km"),
unyt.unyt_array(
[
1000.0,
],
"m",
),
unyt.unyt_array([1.0, 1.0, 1.0], "km"),
),
(
unyt.unyt_array([1.0, 1.0], "km"),
unyt.unyt_quantity(1000.0, "m"),
unyt.unyt_array([1.0, 1.0, 1.0], "km"),
),
(unyt.unyt_array([1.0, 1.0], "km"), 0.5, unyt.unyt_array([1.0, 1.0, 0.5], "km")),
]


@pytest.mark.parametrize("x,x2,expected", _test_cases_insert)
def test_insert_to_unyt_array(x, x2, expected):
result = _mi._insert_to_unyt_array(x, x2, 2)
assert np.all(result == expected)


def test_domain_tracking(domains_to_test):

full_domain = _mi.PhysicalDomainTracker()
Expand Down Expand Up @@ -239,3 +295,23 @@ def test_ref_layer_selection(domains_to_test):

with pytest.raises(ValueError, match="method must be one of"):
_ = _mi._choose_ref_layer(spatial_layer_list, method="not_a_method")


def test_2d_3d_mix():

le = unyt.unyt_array([1.0, 1.0, 1.0], "km")
re = unyt.unyt_array([2000.0, 2000.0, 2000.0], "m")
res = (10, 20, 15)
layer_3d = _mi.LayerDomain(le, re, res)
ref = _mi.ReferenceLayer(layer_3d)

le = unyt.unyt_array([1, 1], "km")
re = unyt.unyt_array([2000.0, 2000.0], "m")
res = (10, 20)
layer_2d = _mi.LayerDomain(
le, re, res, n_d=2, new_dim_value=unyt.unyt_quantity(1, "km")
)

sp_layer = (np.random.random(res), {}, "testname", layer_2d)
new_layer_2d = ref.align_sanitize_layer(sp_layer)
assert "scale" not in new_layer_2d[1] # no scale when it is all 1
Loading