From 7573a12fd814b240b9ebf4a750fd89825b88fa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Thu, 22 Jul 2021 15:39:08 +0200 Subject: [PATCH] ENH: improve API consistency for ProjectionPlot VS SlicePlot: - rename ProjectionPlot -> AxisAlignedProejctionPlot - make SlicePlot a dispatch class instead of a factory function - a similar dispatch class with the name ProjectionPlot - add an abstract layer (NormalPlot) as a common ancestor for plot classes with a "normal"/"axis" argument --- doc/source/analyzing/fields.rst | 15 +- doc/source/cookbook/complex_plots.rst | 2 +- .../cookbook/simple_off_axis_projection.py | 4 +- doc/source/reference/api/api.rst | 1 + doc/source/visualizing/callbacks.rst | 4 +- doc/source/visualizing/plots.rst | 8 +- doc/source/yt4differences.rst | 2 +- yt/__init__.py | 1 + yt/fields/tests/test_field_access.py | 8 +- yt/visualization/api.py | 1 + yt/visualization/plot_window.py | 484 +++++++++++------- yt/visualization/tests/test_plotwindow.py | 76 ++- 12 files changed, 391 insertions(+), 215 deletions(-) diff --git a/doc/source/analyzing/fields.rst b/doc/source/analyzing/fields.rst index fb3ff04163d..0be313dd50b 100644 --- a/doc/source/analyzing/fields.rst +++ b/doc/source/analyzing/fields.rst @@ -643,16 +643,23 @@ this will be handled automatically: .. code-block:: python prj = yt.ProjectionPlot( - ds, "z", ("gas", "velocity_los"), weight_field=("gas", "density") + ds, + "z", + fields=("gas", "velocity_los"), + weight_field=("gas", "density"), ) Which, because the axis is ``"z"``, will give you the same result if you had -projected the ``"velocity_z"`` field. This also works for off-axis projections: +projected the ``"velocity_z"`` field. This also works for off-axis projections, +using an arbitrary normal vector .. code-block:: python - prj = yt.OffAxisProjectionPlot( - ds, [0.1, -0.2, 0.3], ("gas", "velocity_los"), weight_field=("gas", "density") + prj = yt.ProjectionPlot( + ds, + [0.1, -0.2, 0.3], + fields=("gas", "velocity_los"), + weight_field=("gas", "density"), ) diff --git a/doc/source/cookbook/complex_plots.rst b/doc/source/cookbook/complex_plots.rst index ea8c4c6e9d6..4a67b5f23a0 100644 --- a/doc/source/cookbook/complex_plots.rst +++ b/doc/source/cookbook/complex_plots.rst @@ -34,7 +34,7 @@ with a fourth entering for particle data that is deposited onto a mesh is inherited by :class:`~yt.visualization.plot_window.AxisAlignedSlicePlot`, :class:`~yt.visualization.plot_window.OffAxisSlicePlot`, -:class:`~yt.visualization.plot_window.ProjectionPlot`, and +:class:`~yt.visualization.plot_window.AxiAlignedProjectionPlot`, and :class:`~yt.visualization.plot_window.OffAxisProjectionPlot`. This controls the number of resolution elements in the :class:`~yt.visualization.fixed_resolution.FixedResolutionBuffer`, diff --git a/doc/source/cookbook/simple_off_axis_projection.py b/doc/source/cookbook/simple_off_axis_projection.py index 5570c892988..40d56c795b5 100644 --- a/doc/source/cookbook/simple_off_axis_projection.py +++ b/doc/source/cookbook/simple_off_axis_projection.py @@ -13,5 +13,7 @@ # Create an OffAxisProjectionPlot of density centered on the object with the L # vector as its normal and a width of 25 kpc on a side -p = yt.OffAxisProjectionPlot(ds, L, ("gas", "density"), sp.center, (25, "kpc")) +p = yt.ProjectionPlot( + ds, L, fields=("gas", "density"), center=sp.center, width=(25, "kpc") +) p.save() diff --git a/doc/source/reference/api/api.rst b/doc/source/reference/api/api.rst index e0f4a692ab8..f931a1aa763 100644 --- a/doc/source/reference/api/api.rst +++ b/doc/source/reference/api/api.rst @@ -15,6 +15,7 @@ SlicePlot and ProjectionPlot ~yt.visualization.plot_window.AxisAlignedSlicePlot ~yt.visualization.plot_window.OffAxisSlicePlot ~yt.visualization.plot_window.ProjectionPlot + ~yt.visualization.plot_window.AxisAlignedProjectionPlot ~yt.visualization.plot_window.OffAxisProjectionPlot ~yt.visualization.plot_window.WindowPlotMPL ~yt.visualization.plot_window.PlotWindow diff --git a/doc/source/visualizing/callbacks.rst b/doc/source/visualizing/callbacks.rst index ec5898cafbb..7c3567c10c2 100644 --- a/doc/source/visualizing/callbacks.rst +++ b/doc/source/visualizing/callbacks.rst @@ -14,8 +14,10 @@ lines, text, markers, streamlines, velocity vectors, contours, and more. Callbacks can be applied to plots created with :class:`~yt.visualization.plot_window.SlicePlot`, :class:`~yt.visualization.plot_window.ProjectionPlot`, +:class:`~yt.visualization.plot_window.AxisAlignedSlicePlot`, +:class:`~yt.visualization.plot_window.AxisAlignedProjectionPlot`, :class:`~yt.visualization.plot_window.OffAxisSlicePlot`, or -:class:`~yt.visualization.plot_window.OffAxisProjectionPlot` by calling +:class:`~yt.visualization.plot_window.OffAxisProjectionPlot`, by calling one of the ``annotate_`` methods that hang off of the plot object. The ``annotate_`` methods are dynamically generated based on the list of available callbacks. For example: diff --git a/doc/source/visualizing/plots.rst b/doc/source/visualizing/plots.rst index 971ca569f37..de3e936088c 100644 --- a/doc/source/visualizing/plots.rst +++ b/doc/source/visualizing/plots.rst @@ -417,7 +417,7 @@ Here, ``W`` is the width of the projection in the x, y, *and* z directions. One can also generate annotated off axis projections using -:class:`~yt.visualization.plot_window.OffAxisProjectionPlot`. These +:class:`~yt.visualization.plot_window.ProjectionPlot`. These plots can be created in much the same way as an ``OffAxisSlicePlot``, requiring only an open dataset, a direction to project along, and a field to project. For example: @@ -429,12 +429,12 @@ to project along, and a field to project. For example: ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") L = [1, 1, 0] # vector normal to cutting plane north_vector = [-1, 1, 0] - prj = yt.OffAxisProjectionPlot( + prj = yt.ProjectionPlot( ds, L, ("gas", "density"), width=(25, "kpc"), north_vector=north_vector ) prj.save() -OffAxisProjectionPlots can also be created with a number of +``OffAxisProjectionPlot`` objects can also be created with a number of keyword arguments, as described in :class:`~yt.visualization.plot_window.OffAxisProjectionPlot` @@ -584,7 +584,7 @@ Note, the change in the field name from ``("deposit", "nbody_mass")`` to fn = cg.save_as_dataset(fields=[("deposit", "nbody_mass")]) ds_grid = yt.load(fn) - p = yt.OffAxisProjectionPlot(ds_grid, [1, 1, 1], ("grid", "nbody_mass")) + p = yt.ProjectionPlot(ds_grid, [1, 1, 1], ("grid", "nbody_mass")) p.save() Plot Customization: Recentering, Resizing, Colormaps, and More diff --git a/doc/source/yt4differences.rst b/doc/source/yt4differences.rst index b9e4309fdf4..2a59c2c76e3 100644 --- a/doc/source/yt4differences.rst +++ b/doc/source/yt4differences.rst @@ -156,7 +156,7 @@ centre of the pixel and using the standard SPH smoothing formula. The heavy lifting in these functions is undertaken by cython functions. It is now possible to generate slice plots, projection plots, covering grids and -arbitrary grids of smoothed quanitities using these operations. The following +arbitrary grids of smoothed quantities using these operations. The following code demonstrates how this could be achieved. The following would use the scatter method: diff --git a/yt/__init__.py b/yt/__init__.py index bd9aa84f496..994b345235e 100644 --- a/yt/__init__.py +++ b/yt/__init__.py @@ -102,6 +102,7 @@ # Now individual component imports from the visualization API from yt.visualization.api import ( + AxisAlignedProjectionPlot, AxisAlignedSlicePlot, FITSImageData, FITSOffAxisProjection, diff --git a/yt/fields/tests/test_field_access.py b/yt/fields/tests/test_field_access.py index 14951bdc0fe..f55a0c1500e 100644 --- a/yt/fields/tests/test_field_access.py +++ b/yt/fields/tests/test_field_access.py @@ -1,10 +1,6 @@ from yt.data_objects.profiles import create_profile from yt.testing import assert_equal, fake_random_ds -from yt.visualization.plot_window import ( - OffAxisProjectionPlot, - ProjectionPlot, - SlicePlot, -) +from yt.visualization.plot_window import ProjectionPlot, SlicePlot from yt.visualization.profile_plotter import PhasePlot, ProfilePlot @@ -29,7 +25,7 @@ def test_field_access(): s = SlicePlot(ds, 2, field) oas = SlicePlot(ds, [1, 1, 1], field) p = ProjectionPlot(ds, 2, field) - oap = OffAxisProjectionPlot(ds, [1, 1, 1], field) + oap = ProjectionPlot(ds, [1, 1, 1], field) for plot_object in [s, oas, p, oap, prof, phase]: plot_object._setup_plots() diff --git a/yt/visualization/api.py b/yt/visualization/api.py index 3bd63bbb5ff..12bc0e6019b 100644 --- a/yt/visualization/api.py +++ b/yt/visualization/api.py @@ -23,6 +23,7 @@ from .particle_plots import ParticlePhasePlot, ParticlePlot, ParticleProjectionPlot from .plot_modifications import PlotCallback, callback_registry from .plot_window import ( + AxisAlignedProjectionPlot, AxisAlignedSlicePlot, OffAxisProjectionPlot, OffAxisSlicePlot, diff --git a/yt/visualization/plot_window.py b/yt/visualization/plot_window.py index 14c4cff0571..76fc0473b16 100644 --- a/yt/visualization/plot_window.py +++ b/yt/visualization/plot_window.py @@ -1,6 +1,8 @@ +import abc from collections import defaultdict from functools import wraps from numbers import Number +from typing import Union import matplotlib import matplotlib.pyplot as plt @@ -1443,7 +1445,288 @@ class to create the new figure layout. return fig -class AxisAlignedSlicePlot(PWViewerMPL): +class NormalPlot(abc.ABC): + """This is the abstraction for SlicePlot and ProjectionPlot, where + we define the common sanitizing mechanism for user input (normal direction). + """ + + @staticmethod + def sanitize_normal_vector(ds, normal) -> Union[str, np.ndarray]: + """Return the name of a cartesian axis whener possible, + or a 3-element 1D ndarray of float64 in any other valid case. + Fail with a descriptive error message otherwise. + """ + axis_names = ds.coordinates.axis_order + + if isinstance(normal, str): + if normal not in axis_names: + names_str = ", ".join(f"'{name}'" for name in axis_names) + raise ValueError( + f"'{normal}' is not a valid axis name. Expected one of {names_str}." + ) + return normal + + if isinstance(normal, (int, np.integer)): + if normal not in (0, 1, 2): + raise ValueError( + f"{normal} is not a valid axis identifier. Expected either 0, 1, or 2." + ) + return axis_names[normal] + + if not is_sequence(normal): + raise TypeError( + f"{normal} is not a valid normal vector identifier. " + "Expected a string, integer or sequence of 3 floats." + ) + + if len(normal) != 3: + raise ValueError( + f"{normal} with length {len(normal)} is not a valid normal vector. " + "Expected a 3-element sequence." + ) + + try: + retv = np.array(normal, dtype="float64") + if retv.shape != (3,): + raise ValueError(f"{normal} is incorrectly shaped.") + except ValueError as exc: + raise TypeError(f"{normal} is not a valid normal vector.") from exc + + nonzero_idx = np.nonzero(retv)[0] + if len(nonzero_idx) == 0: + raise ValueError(f"A null vector {normal} isn't a valid normal vector.") + if len(nonzero_idx) == 1: + return axis_names[nonzero_idx[0]] + + return retv + + +class SlicePlot(NormalPlot): + r""" + A dispatch class for :class:`yt.visualization.plot_window.AxisAlignedSlicePlot` + and :class:`yt.visualization.plot_window.OffAxisSlicePlot` objects. This + essentially allows for a single entry point to both types of slice plots, + the distinction being determined by the specified normal vector to the + projection. + + The returned plot object can be updated using one of the many helper + functions defined in PlotWindow. + + Parameters + ---------- + + ds : :class:`yt.data_objects.static_output.Dataset` + This is the dataset object corresponding to the + simulation output to be plotted. + normal : int, str, or 3-element sequence of floats + This specifies the normal vector to the slice. + Valid int values are 0, 1 and 2. Coresponding str values depend on the + geometry of the dataset and are generally given by `ds.coordinates.axis_order`. + E.g. in cartesian they are 'x', 'y' and 'z'. + An arbitrary normal vector may be specified as a 3-element sequence of floats. + + This returns a :class:`OffAxisSlicePlot` object or a + :class:`AxisAlignedSlicePlot` object, depending on wether the requested + normal directions corresponds to a natural axis of the dataset's geometry. + + fields : a (or a list of) 2-tuple of strings (ftype, fname) + The name of the field(s) to be plotted. + + The following are nominally keyword arguments passed onto the respective + slice plot objects generated by this function. + + Keyword Arguments + ----------------- + + center : A sequence floats, a string, or a tuple. + The coordinate of the center of the image. If set to 'c', 'center' or + left blank, the plot is centered on the middle of the domain. If set to + 'max' or 'm', the center will be located at the maximum of the + ('gas', 'density') field. Centering on the max or min of a specific + field is supported by providing a tuple such as ("min","temperature") or + ("max","dark_matter_density"). Units can be specified by passing in *center* + as a tuple containing a coordinate and string unit name or by passing + in a YTArray. If a list or unitless array is supplied, code units are + assumed. + width : tuple or a float. + Width can have four different formats to support windows with variable + x and y widths. They are: + + ================================== ======================= + format example + ================================== ======================= + (float, string) (10,'kpc') + ((float, string), (float, string)) ((10,'kpc'),(15,'kpc')) + float 0.2 + (float, float) (0.2, 0.3) + ================================== ======================= + + For example, (10, 'kpc') requests a plot window that is 10 kiloparsecs + wide in the x and y directions, ((10,'kpc'),(15,'kpc')) requests a + window that is 10 kiloparsecs wide along the x axis and 15 + kiloparsecs wide along the y axis. In the other two examples, code + units are assumed, for example (0.2, 0.3) requests a plot that has an + x width of 0.2 and a y width of 0.3 in code units. If units are + provided the resulting plot axis labels will use the supplied units. + axes_unit : string + The name of the unit for the tick labels on the x and y axes. + Defaults to None, which automatically picks an appropriate unit. + If axes_unit is '1', 'u', or 'unitary', it will not display the + units, and only show the axes name. + origin : string or length 1, 2, or 3 sequence. + The location of the origin of the plot coordinate system for + `AxisAlignedSlicePlot` object; for `OffAxisSlicePlot` objects this + parameter is discarded. This is typically represented by a '-' + separated string or a tuple of strings. In the first index the + y-location is given by 'lower', 'upper', or 'center'. The second index + is the x-location, given as 'left', 'right', or 'center'. Finally, the + whether the origin is applied in 'domain' space, plot 'window' space or + 'native' simulation coordinate system is given. For example, both + 'upper-right-domain' and ['upper', 'right', 'domain'] place the + origin in the upper right hand corner of domain space. If x or y + are not given, a value is inferred. For instance, 'left-domain' + corresponds to the lower-left hand corner of the simulation domain, + 'center-domain' corresponds to the center of the simulation domain, + or 'center-window' for the center of the plot window. In the event + that none of these options place the origin in a desired location, + a sequence of tuples and a string specifying the + coordinate space can be given. If plain numeric types are input, + units of `code_length` are assumed. Further examples: + + =============================================== =============================== + format example + =============================================== =============================== + '{space}' 'domain' + '{xloc}-{space}' 'left-window' + '{yloc}-{space}' 'upper-domain' + '{yloc}-{xloc}-{space}' 'lower-right-window' + ('{space}',) ('window',) + ('{xloc}', '{space}') ('right', 'domain') + ('{yloc}', '{space}') ('lower', 'window') + ('{yloc}', '{xloc}', '{space}') ('lower', 'right', 'window') + ((yloc, '{unit}'), (xloc, '{unit}'), '{space}') ((0, 'm'), (.4, 'm'), 'window') + (xloc, yloc, '{space}') (0.23, 0.5, 'domain') + =============================================== =============================== + north_vector : a sequence of floats + A vector defining the 'up' direction in the `OffAxisSlicePlot`; not + used in `AxisAlignedSlicePlot`. This option sets the orientation of the + slicing plane. If not set, an arbitrary grid-aligned north-vector is + chosen. + fontsize : integer + The size of the fonts for the axis, colorbar, and tick labels. + field_parameters : dictionary + A dictionary of field parameters than can be accessed by derived + fields. + data_source : YTSelectionContainer Object + Object to be used for data selection. Defaults to a region covering + the entire simulation. + + Raises + ------ + + ValueError or TypeError + If `normal` cannot be interpreted as a valid normal direction. + + Examples + -------- + + >>> from yt import load + >>> ds = load("IsolatedGalaxy/galaxy0030/galaxy0030") + >>> slc = SlicePlot(ds, "x", ("gas", "density"), center=[0.2, 0.3, 0.4]) + + >>> slc = SlicePlot( + ... ds, [0.4, 0.2, -0.1], ("gas", "pressure"), north_vector=[0.2, -0.3, 0.1] + ... ) + + """ + + def __new__( + cls, ds, normal, fields, *args, **kwargs + ) -> Union["AxisAlignedSlicePlot", "OffAxisSlicePlot"]: + if cls is SlicePlot: + normal = cls.sanitize_normal_vector(ds, normal) + if isinstance(normal, str): + # north_vector not used in AxisAlignedSlicePlots; remove it if in kwargs + if "north_vector" in kwargs: + mylog.warning( + "Ignoring 'north_vector' keyword as it is ill-defined for " + "an AxisAlignedSlicePlot object." + ) + del kwargs["north_vector"] + + cls = AxisAlignedSlicePlot + else: + # OffAxisSlicePlot has hardcoded origin; remove it if in kwargs + if "origin" in kwargs: + mylog.warning( + "Ignoring 'origin' keyword as it is ill-defined for " + "an OffAxisSlicePlot object." + ) + del kwargs["origin"] + + cls = OffAxisSlicePlot + self = object.__new__(cls) + return self + + +class ProjectionPlot(NormalPlot): + r""" + A dispatch class for :class:`yt.visualization.plot_window.AxisAlignedProjectionPlot` + and :class:`yt.visualization.plot_window.OffAxisProjectionPlot` objects. This + essentially allows for a single entry point to both types of projection plots, + the distinction being determined by the specified normal vector to the + slice. + + The returned plot object can be updated using one of the many helper + functions defined in PlotWindow. + + Parameters + ---------- + + ds : :class:`yt.data_objects.static_output.Dataset` + This is the dataset object corresponding to the + simulation output to be plotted. + normal : int, str, or 3-element sequence of floats + This specifies the normal vector to the slice. + Valid int values are 0, 1 and 2. Coresponding str values depend on the + geometry of the dataset and are generally given by `ds.coordinates.axis_order`. + E.g. in cartesian they are 'x', 'y' and 'z'. + An arbitrary normal vector may be specified as a 3-element sequence of floats. + + This function will return a :class:`OffAxisSlicePlot` object or a + :class:`AxisAlignedSlicePlot` object, depending on wether the requested + normal directions corresponds to a natural axis of the dataset's geometry. + + fields : a (or a list of) 2-tuple of strings (ftype, fname) + The name of the field(s) to be plotted. + + + Any additional positional and keyword arguments are passed down to the appropriate + return class. See :class:`yt.visualization.plot_window.AxisAlignedProjectionPlot` + and :class:`yt.visualization.plot_window.OffAxisProjectionPlot`. + + Raises + ------ + + ValueError or TypeError + If `normal` cannot be interpreted as a valid normal direction. + + """ + + def __new__( + cls, ds, normal, fields, *args, **kwargs + ) -> Union["AxisAlignedProjectionPlot", "OffAxisProjectionPlot"]: + if cls is ProjectionPlot: + normal = cls.sanitize_normal_vector(ds, normal) + if isinstance(normal, str): + cls = AxisAlignedProjectionPlot + else: + cls = OffAxisProjectionPlot + self = object.__new__(cls) + return self + + +class AxisAlignedSlicePlot(SlicePlot, PWViewerMPL): r"""Creates a slice plot from a dataset Given a ds object, an axis to slice along, and a field name @@ -1628,7 +1911,7 @@ def __init__( self.set_axes_unit(axes_unit) -class ProjectionPlot(PWViewerMPL): +class AxisAlignedProjectionPlot(ProjectionPlot, PWViewerMPL): r"""Creates a projection plot from a dataset Given a ds object, an axis to project along, and a field name @@ -1772,7 +2055,7 @@ class ProjectionPlot(PWViewerMPL): >>> from yt import load >>> ds = load("IsolateGalaxygalaxy0030/galaxy0030") - >>> p = ProjectionPlot(ds, "z", ("gas", "density"), width=(20, "kpc")) + >>> p = AxisAlignedProjectionPlot(ds, "z", ("gas", "density"), width=(20, "kpc")) """ _plot_type = "Projection" @@ -1868,7 +2151,7 @@ def __init__( self.set_axes_unit(axes_unit) -class OffAxisSlicePlot(PWViewerMPL): +class OffAxisSlicePlot(SlicePlot, PWViewerMPL): r"""Creates an off axis slice plot from a dataset Given a ds object, a normal vector defining a slicing plane, and @@ -2047,7 +2330,7 @@ def _determine_fields(self, *args): return self.dd._determine_fields(*args) -class OffAxisProjectionPlot(PWViewerMPL): +class OffAxisProjectionPlot(ProjectionPlot, PWViewerMPL): r"""Creates an off axis projection plot from a dataset Given a ds object, a normal vector to project along, and @@ -2293,197 +2576,6 @@ def _create_axes(self, axrect): self.axes = self.figure.add_axes(axrect, projection=self._projection) -def SlicePlot(ds, normal=None, fields=None, axis=None, *args, **kwargs): - r""" - A factory function for - :class:`yt.visualization.plot_window.AxisAlignedSlicePlot` - and :class:`yt.visualization.plot_window.OffAxisSlicePlot` objects. This - essentially allows for a single entry point to both types of slice plots, - the distinction being determined by the specified normal vector to the - slice. - - The returned plot object can be updated using one of the many helper - functions defined in PlotWindow. - - Parameters - ---------- - - ds : :class:`yt.data_objects.static_output.Dataset` - This is the dataset object corresponding to the - simulation output to be plotted. - normal : int or one of 'x', 'y', 'z', or sequence of floats - This specifies the normal vector to the slice. If given as an integer - or a coordinate string (0=x, 1=y, 2=z), this function will return an - :class:`AxisAlignedSlicePlot` object. If given as a sequence of floats, - this is interpreted as an off-axis vector and an - :class:`OffAxisSlicePlot` object is returned. - fields : string - The name of the field(s) to be plotted. - axis : int or one of 'x', 'y', 'z' - An int corresponding to the axis to slice along (0=x, 1=y, 2=z) - or the axis name itself. If specified, this will replace normal. - - - The following are nominally keyword arguments passed onto the respective - slice plot objects generated by this function. - - Keyword Arguments - ----------------- - - center : A sequence floats, a string, or a tuple. - The coordinate of the center of the image. If set to 'c', 'center' or - left blank, the plot is centered on the middle of the domain. If set to - 'max' or 'm', the center will be located at the maximum of the - ('gas', 'density') field. Centering on the max or min of a specific - field is supported by providing a tuple such as ("min","temperature") or - ("max","dark_matter_density"). Units can be specified by passing in *center* - as a tuple containing a coordinate and string unit name or by passing - in a YTArray. If a list or unitless array is supplied, code units are - assumed. - width : tuple or a float. - Width can have four different formats to support windows with variable - x and y widths. They are: - - ================================== ======================= - format example - ================================== ======================= - (float, string) (10,'kpc') - ((float, string), (float, string)) ((10,'kpc'),(15,'kpc')) - float 0.2 - (float, float) (0.2, 0.3) - ================================== ======================= - - For example, (10, 'kpc') requests a plot window that is 10 kiloparsecs - wide in the x and y directions, ((10,'kpc'),(15,'kpc')) requests a - window that is 10 kiloparsecs wide along the x axis and 15 - kiloparsecs wide along the y axis. In the other two examples, code - units are assumed, for example (0.2, 0.3) requests a plot that has an - x width of 0.2 and a y width of 0.3 in code units. If units are - provided the resulting plot axis labels will use the supplied units. - axes_unit : string - The name of the unit for the tick labels on the x and y axes. - Defaults to None, which automatically picks an appropriate unit. - If axes_unit is '1', 'u', or 'unitary', it will not display the - units, and only show the axes name. - origin : string or length 1, 2, or 3 sequence. - The location of the origin of the plot coordinate system for - `AxisAlignedSlicePlot` object; for `OffAxisSlicePlot` objects this - parameter is discarded. This is typically represented by a '-' - separated string or a tuple of strings. In the first index the - y-location is given by 'lower', 'upper', or 'center'. The second index - is the x-location, given as 'left', 'right', or 'center'. Finally, the - whether the origin is applied in 'domain' space, plot 'window' space or - 'native' simulation coordinate system is given. For example, both - 'upper-right-domain' and ['upper', 'right', 'domain'] place the - origin in the upper right hand corner of domain space. If x or y - are not given, a value is inferred. For instance, 'left-domain' - corresponds to the lower-left hand corner of the simulation domain, - 'center-domain' corresponds to the center of the simulation domain, - or 'center-window' for the center of the plot window. In the event - that none of these options place the origin in a desired location, - a sequence of tuples and a string specifying the - coordinate space can be given. If plain numeric types are input, - units of `code_length` are assumed. Further examples: - - =============================================== =============================== - format example - =============================================== =============================== - '{space}' 'domain' - '{xloc}-{space}' 'left-window' - '{yloc}-{space}' 'upper-domain' - '{yloc}-{xloc}-{space}' 'lower-right-window' - ('{space}',) ('window',) - ('{xloc}', '{space}') ('right', 'domain') - ('{yloc}', '{space}') ('lower', 'window') - ('{yloc}', '{xloc}', '{space}') ('lower', 'right', 'window') - ((yloc, '{unit}'), (xloc, '{unit}'), '{space}') ((0, 'm'), (.4, 'm'), 'window') - (xloc, yloc, '{space}') (0.23, 0.5, 'domain') - =============================================== =============================== - north_vector : a sequence of floats - A vector defining the 'up' direction in the `OffAxisSlicePlot`; not - used in `AxisAlignedSlicePlot`. This option sets the orientation of the - slicing plane. If not set, an arbitrary grid-aligned north-vector is - chosen. - fontsize : integer - The size of the fonts for the axis, colorbar, and tick labels. - field_parameters : dictionary - A dictionary of field parameters than can be accessed by derived - fields. - data_source : YTSelectionContainer Object - Object to be used for data selection. Defaults to a region covering - the entire simulation. - - Raises - ------ - - AssertionError - If a proper normal axis is not specified via the normal or axis - keywords, and/or if a field to plot is not specified. - - Examples - -------- - - >>> from yt import load - >>> ds = load("IsolatedGalaxy/galaxy0030/galaxy0030") - >>> slc = SlicePlot(ds, "x", ("gas", "density"), center=[0.2, 0.3, 0.4]) - - >>> slc = SlicePlot( - ... ds, [0.4, 0.2, -0.1], ("gas", "pressure"), north_vector=[0.2, -0.3, 0.1] - ... ) - - """ - if axis is not None: - issue_deprecation_warning( - "SlicePlot's argument 'axis' is a deprecated alias for 'normal', it " - "will be removed in a future version of yt.", - since="4.0.0", - removal="4.1.0", - ) - if normal is not None: - raise TypeError( - "SlicePlot() received incompatible arguments 'axis' and 'normal'" - ) - normal = axis - - # to keep positional ordering we had to make 'normal' and 'fields' keywords - if normal is None: - raise TypeError("Missing argument in SlicePlot(): 'normal'") - - if fields is None: - raise TypeError("Missing argument in SlicePlot(): 'fields'") - - # use an AxisAlignedSlicePlot where possible, e.g.: - # maybe someone passed normal=[0,0,0.2] when they should have just used "z" - if is_sequence(normal) and not isinstance(normal, str): - if np.count_nonzero(normal) == 1: - normal = ("x", "y", "z")[np.nonzero(normal)[0][0]] - else: - normal = np.array(normal, dtype="float64") - np.divide(normal, np.dot(normal, normal), normal) - - # by now the normal should be properly set to get either a On/Off Axis plot - if is_sequence(normal) and not isinstance(normal, str): - # OffAxisSlicePlot has hardcoded origin; remove it if in kwargs - if "origin" in kwargs: - mylog.warning( - "Ignoring 'origin' keyword as it is ill-defined for " - "an OffAxisSlicePlot object." - ) - del kwargs["origin"] - - return OffAxisSlicePlot(ds, normal, fields, *args, **kwargs) - else: - # north_vector not used in AxisAlignedSlicePlots; remove it if in kwargs - if "north_vector" in kwargs: - mylog.warning( - "Ignoring 'north_vector' keyword as it is ill-defined for " - "an AxisAlignedSlicePlot object." - ) - del kwargs["north_vector"] - - return AxisAlignedSlicePlot(ds, normal, fields, *args, **kwargs) - - def plot_2d( ds, fields, diff --git a/yt/visualization/tests/test_plotwindow.py b/yt/visualization/tests/test_plotwindow.py index cba87d757ee..8d8a01d2462 100644 --- a/yt/visualization/tests/test_plotwindow.py +++ b/yt/visualization/tests/test_plotwindow.py @@ -16,6 +16,7 @@ assert_fname, assert_raises, assert_rel_equal, + fake_amr_ds, fake_random_ds, requires_file, ) @@ -27,7 +28,10 @@ requires_ds, ) from yt.utilities.exceptions import YTInvalidFieldType -from yt.visualization.api import ( +from yt.visualization.plot_window import ( + AxisAlignedProjectionPlot, + AxisAlignedSlicePlot, + NormalPlot, OffAxisProjectionPlot, OffAxisSlicePlot, ProjectionPlot, @@ -731,3 +735,73 @@ def test_nan_data(): with tempfile.NamedTemporaryFile(suffix="png") as f: plot.save(f.name) + + +def test_sanitize_valid_normal_vector(): + # note: we don't test against non-cartesian geometries + # because the way normal "vectors" work isn't cleary + # specified and works more as an implementation detail + # at the moment + ds = fake_amr_ds(geometry="cartesian") + + # We allow maximal polymorphism for axis-aligned directions: + # even if 3-component vector is received, we want to use the + # AxisAligned* plotting class (as opposed to OffAxis*) because + # it's much easier to optimize so it's expected to be more + # performant. + axis_label_from_inputs = { + "x": ["x", 0, [1, 0, 0], [0.1, 0.0, 0.0], [-10, 0, 0]], + "y": ["y", 1, [0, 1, 0], [0.0, 0.1, 0.0], [0, -10, 0]], + "z": ["z", 2, [0, 0, 1], [0.0, 0.0, 0.1], [0, 0, -10]], + } + for expected, user_inputs in axis_label_from_inputs.items(): + for ui in user_inputs: + assert NormalPlot.sanitize_normal_vector(ds, ui) == expected + + # arbitrary 3-floats sequences are also valid input. + # They should be returned as np.ndarrays, but the norm and orientation + # could be altered. What's important is that their direction is preserved. + for ui in [(1, 1, 1), [0.0, -3, 1e9], np.ones(3, dtype="int8")]: + res = NormalPlot.sanitize_normal_vector(ds, ui) + assert isinstance(res, np.ndarray) + assert res.dtype == np.float64 + assert_array_equal( + np.cross(ui, res), + [0, 0, 0], + ) + + +def test_reject_invalid_normal_vector(): + ds = fake_amr_ds(geometry="cartesian") + for ui in [0.0, 1.0, 2.0, 3.0]: + # acceptable scalar numeric values are restricted to integers. + # Floats might be a sign that something went wrong upstream + # e.g., rounding errors, parsing error... + assert_raises(TypeError, NormalPlot.sanitize_normal_vector, ds, ui) + for ui in [ + "X", + "xy", + "not-an-axis", + (0, 0, 0), + [0, 0, 0], + np.zeros(3), + [1, 0, 0, 0], + [1, 0], + [1], + [0], + 3, + 10, + ]: + assert_raises(ValueError, NormalPlot.sanitize_normal_vector, ds, ui) + + +def test_dispatch_plot_classes(): + ds = fake_random_ds(16) + p1 = ProjectionPlot(ds, "z", ("gas", "density")) + p2 = ProjectionPlot(ds, (1, 2, 3), ("gas", "density")) + s1 = SlicePlot(ds, "z", ("gas", "density")) + s2 = SlicePlot(ds, (1, 2, 3), ("gas", "density")) + assert isinstance(p1, AxisAlignedProjectionPlot) + assert isinstance(p2, OffAxisProjectionPlot) + assert isinstance(s1, AxisAlignedSlicePlot) + assert isinstance(s2, OffAxisSlicePlot)