From cf2078883d24cbe36e8735497c9b1a2a3dcfad3b Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Thu, 19 Feb 2026 15:18:13 +1100 Subject: [PATCH 1/3] Update transect implementation The Transect class can now transect 2D surface variables, not just 3d volumes. The plotting functions have been moved to the module level. New CrossSectionArtist and TransectStepArtists have been added, much like the new Artists for surface plotting. Tests and docs to come. --- docs/api/transect.rst | 59 +- docs/conf.py | 3 +- docs/releases/development.rst | 5 + examples/plot-animated-transect.py | 93 ++ examples/plot-kgari-transect.py | 124 ++- src/emsarray/transect.py | 807 ------------------ src/emsarray/transect/__init__.py | 8 + src/emsarray/transect/artists.py | 130 +++ src/emsarray/transect/base.py | 609 +++++++++++++ src/emsarray/transect/utils.py | 194 +++++ .../test_plot.py} | 7 + 11 files changed, 1192 insertions(+), 847 deletions(-) create mode 100644 examples/plot-animated-transect.py delete mode 100644 src/emsarray/transect.py create mode 100644 src/emsarray/transect/__init__.py create mode 100644 src/emsarray/transect/artists.py create mode 100644 src/emsarray/transect/base.py create mode 100644 src/emsarray/transect/utils.py rename tests/{test_transect.py => transect/test_plot.py} (92%) diff --git a/docs/api/transect.rst b/docs/api/transect.rst index a4c58cf..a4b5484 100644 --- a/docs/api/transect.rst +++ b/docs/api/transect.rst @@ -1,24 +1,65 @@ -.. module:: emsarray.transect ================= emsarray.transect ================= -.. currentmodule:: emsarray.transect +.. module:: emsarray.transect -Plot transects through your dataset. -Transects are vertical slices along some path through your dataset. +This module provides methods for extracting and plotting data +along transects through your datasets. +A transect path is represented as a :class:`shapely.LineString`. +Data along the transect can be extracted in to a new :class:`xarray.Dataset`, +or plotted using :meth:`Transect.make_artist`. + +Currently it is only possible to take transects through grids with polygonal geometry. +Taking transects through other kinds of geometry is a planned future enhancement. Examples --------- +======== -.. minigallery:: ../examples/plot-kgari-transect.py +.. minigallery:: -.. autofunction:: plot + ../examples/plot-kgari-transect.py + ../examples/plot-animated-transect.py + +Transects +========= + +These classes find the intersection of a :class:`shapely.LineString` with a dataset +and provide methods to introspect this intersection, plot data along this path, +and extract data along this path. .. autoclass:: Transect :members: -.. autoclass:: TransectPoint +.. autoclass:: TransectPoint() + :members: + +.. autoclass:: TransectSegment() + :members: + +Artists +======= + +These classes plot data along a transect. +Transect artists are normally created by calling :meth:`.Transect.make_artist`. + +.. module:: emsarray.transect.artists + +.. autoclass:: TransectArtist() + :members: set_data_array + +.. autoclass:: CrossSectionArtist() + :members: from_transect -.. autoclass:: TransectSegment +.. autoclass:: TransectStepArtist() + :members: from_transect + +Utilities +========= + +.. currentmodule:: emsarray.transect + +.. autofunction:: plot +.. autofunction:: setup_distance_axis +.. autofunction:: setup_depth_axis diff --git a/docs/conf.py b/docs/conf.py index 5d8d49d..694bfca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -111,6 +111,7 @@ def setup(app): 'examples_dirs': '../examples', 'gallery_dirs': './examples', 'filename_pattern': '/plot-', - 'matplotlib_animations': True, + 'matplotlib_animations': (True, 'jshtml'), 'backreferences_dir': './examples/backreferences', + 'remove_config_comments': True, } diff --git a/docs/releases/development.rst b/docs/releases/development.rst index 3710844..90383fa 100644 --- a/docs/releases/development.rst +++ b/docs/releases/development.rst @@ -29,3 +29,8 @@ Next release (in development) (:pr:`221`). * Bounds variables are now included in :meth:`Convention.get_all_geometry_names()` (:pr:`222`). +* Allow transecting two-dimensional variables, such as sea surface height + (:issue:`197`, :pr:`216`). +* Add :meth:`.Transect.make_artist()` method and the related + :class:`.CrossSectionArtist` and :class:`.TransectStepArtist` artists + (:issue:`175`, :pr:`216`). diff --git a/examples/plot-animated-transect.py b/examples/plot-animated-transect.py new file mode 100644 index 0000000..de8a314 --- /dev/null +++ b/examples/plot-animated-transect.py @@ -0,0 +1,93 @@ +""" +================= +Animated transect +================= + +Transect and cross section plots can be animated using +:meth:`.TransectArtist.set_data_array()` to update the data. +""" +import datetime + +import shapely +import matplotlib.pyplot as plt +import pandas +import xarray +from matplotlib.artist import Artist +from matplotlib.animation import FuncAnimation +from matplotlib.colors import LogNorm +from matplotlib.ticker import ScalarFormatter, EngFormatter + +import emsarray +from emsarray import transect, utils +from emsarray.operations import depth + +dataset = emsarray.tutorial.open_dataset('kgari') + +# Select only the variables we want to plot. +dataset = dataset.ems.select_variables(['eta']) + +# %% +# The following is a :mod:`transect ` path +# starting in the Great Sandy Strait near K'gari, +# heading roughly North out to deeper waters: +north_transect = transect.Transect(dataset, shapely.LineString([ + [152.9768944, -25.4827962], + [152.9701996, -25.4420345], + [152.9727745, -25.3967620], + [152.9623032, -25.3517828], + [152.9401588, -25.3103560], + [152.9173279, -25.2538563], + [152.8962135, -25.1942238], + [152.8692627, -25.0706729], + [152.8623962, -24.9698750], + [152.8472900, -24.8415806], + [152.8308105, -24.6470172], +])) + + +# %% +# Now we set up a figure and add a cross section artist. + +# sphinx_gallery_defer_figures +# sphinx_gallery_capture_repr_block = () + +figure = plt.figure(figsize=(7.8, 3), layout='constrained', dpi=100) +axes = figure.add_subplot() + +axes.set_title('Sea surface height above geoid') + +# Draw the sea surface height +eta = dataset['eta'] +eta_artist = north_transect.make_artist( + axes, eta.isel(time=0)) + +# Add a time label +aest = datetime.timezone(datetime.timedelta(hours=10)) +datetime_labels = [ + utils.datetime_from_np_time(date).astimezone(aest).strftime('%Y-%m-%d %H:%M') + for date in dataset['time'].values +] +datetime_annotation = axes.annotate( + datetime_labels[0], + xy=(5, 5), xycoords='axes points', + verticalalignment='bottom', horizontalalignment='left') + +# Set up the axes +transect.setup_distance_axis( + north_transect, axes) +axes.set_ylim(-1.5, 1.5) +axes.set_ylabel("Height") +axes.yaxis.set_major_formatter('{x:1.1f} m') +axes.axhline(y=0, linestyle='--', color='grey', linewidth=0.5) + +# %% +# Finally we set up the animation. +# The ``update()`` function is called every frame to update the plot with new data. +# The :meth:`.TransectArtist.set_data_array()` function does all the hard work here. + +def update(frame: int) -> list[Artist]: + eta_artist.set_data_array(eta.isel(time=frame)) + datetime_annotation.set_text(datetime_labels[frame]) + return [eta_artist, datetime_annotation] + +animation = FuncAnimation(figure, update, frames=eta.sizes['time']) diff --git a/examples/plot-kgari-transect.py b/examples/plot-kgari-transect.py index 136597c..e00780c 100644 --- a/examples/plot-kgari-transect.py +++ b/examples/plot-kgari-transect.py @@ -5,20 +5,31 @@ """ import shapely -from matplotlib import pyplot +import matplotlib.pyplot as plt +from matplotlib import gridspec +from matplotlib.colors import LogNorm, PowerNorm import emsarray -from emsarray import plot, transect +from emsarray import plot, transect, utils +from emsarray.operations import depth -dataset_url = 'https://thredds.nci.org.au/thredds/dodsC/fx3/gbr4_H4p0_ABARRAr2_OBRAN2020_FG2Gv3_Dhnd/gbr4_simple_2022-10-31.nc' -dataset = emsarray.open_dataset(dataset_url).isel(time=-1) -dataset = dataset.ems.select_variables(['botz', 'temp']) +dataset_url = 'https://thredds.nci.org.au/thredds/dodsC/fx3/gbr4_H4p0_ABARRAr2_OBRAN2020_FG2Gv3_Dhnd/gbr4_simple_2022-10-01.nc' +# dataset_url = '~/example-datasets/gbr4_simple_2022-10-31.nc' +dataset = emsarray.open_dataset(dataset_url).isel(time=12) +# Select only the variables we want to plot. +dataset = dataset.ems.select_variables(['botz', 'temp', 'eta']) +# The depth coordinate has positive=up, while the bathymetry has positive=down. +# This causes issues when drawing the ocean floor. +# Lets fix the depth coordinate. +dataset = depth.normalize_depth_variables(dataset, ['zc'], positive_down=True) +# Cross section plots need bounds information, so lets invent some +dataset = utils.estimate_bounds_1d(dataset, 'zc') # %% # The following is a :mod:`transect ` path # starting in the Great Sandy Strait near K'gari, # heading roughly North out to deeper waters: -line = shapely.LineString([ +north_transect = transect.Transect(dataset, shapely.LineString([ [152.9768944, -25.4827962], [152.9701996, -25.4420345], [152.9727745, -25.3967620], @@ -33,41 +44,94 @@ [152.7607727, -24.3521012], [152.6392365, -24.1906056], [152.4792480, -24.0615124], -]) +])) landmarks = [ ('Round Island', shapely.Point(152.9262543, -25.2878719)), ('Lady Elliot Island', shapely.Point(152.7145958, -24.1129146)), ] + # %% -# Plot a transect showing temperature along this path. +# Set up three axes: one showing the transect path, +# one showing a temperature cross section along the transect, +# and one showing the sea surface height along the transect. + +# sphinx_gallery_defer_figures -figure = transect.plot( - dataset, line, dataset['temp'], - figsize=(7.9, 3), - bathymetry=dataset['botz'], - landmarks=landmarks, - title="Temperature", - cmap='Oranges_r') -pyplot.show() +figure = plt.figure(figsize=(7.8, 8), layout='constrained', dpi=100) +gs_root = gridspec.GridSpec(3, 1, figure=figure, height_ratios=[3, 1, 1]) +path_axes = figure.add_subplot(gs_root[0], projection=dataset.ems.data_crs) +temp_axes = figure.add_subplot(gs_root[1]) +eta_axes = figure.add_subplot(gs_root[2], sharex=temp_axes) # %% -# The path of the transect can be plotted using matplotlib. +# First make a plot showing the path of the transect overlayed on the bathymetry + +# sphinx_gallery_defer_figures +# sphinx_gallery_capture_repr_block = () -# Plot the path of the transect -figure = pyplot.figure(figsize=(5, 5), dpi=100) -axes = figure.add_subplot(projection=dataset.ems.data_crs) -axes.set_aspect(aspect='equal', adjustable='datalim') -axes.set_title('Transect path') +path_axes.set_aspect(aspect='equal', adjustable='datalim') +path_axes.set_title('Transect path') dataset.ems.make_artist( - axes, 'botz', cmap='Blues', clim=(0, 2000), edgecolor='face', + path_axes, 'botz', cmap='Blues', clim=(0, 2000), edgecolor='face', + norm=PowerNorm(gamma=0.5), linewidth=0.5, zorder=0) -axes = figure.axes[0] -axes.set_extent(plot.bounds_to_extent(line.envelope.buffer(0.2).bounds)) -axes.plot(*line.coords.xy, zorder=2, c='orange', linewidth=4) +path_axes.set_extent(plot.bounds_to_extent(north_transect.line.envelope.buffer(0.2).bounds)) +path_axes.plot(*north_transect.line.coords.xy, zorder=1, c='orange', linewidth=4) + +plot.add_coast(path_axes, zorder=1) +plot.add_gridlines(path_axes) +plot.add_landmarks(path_axes, landmarks) + +# %% +# Now plot a cross section along the transect showing the ocean temperature. +# As the temperature variable has a depth axis the cross section is two dimensional. + +# sphinx_gallery_defer_figures +# sphinx_gallery_capture_repr_block = () + +temp_axes.set_title('Temperature') +dataset['temp'].attrs['units'] = '°C' +dataset['zc'].attrs['long_name'] = 'Depth' + +north_transect.make_artist( + temp_axes, 'temp', cmap='plasma') +north_transect.make_ocean_floor_artist( + temp_axes, dataset['botz']) +# yaxis +transect.setup_depth_axis( + north_transect, temp_axes, data_array='temp', + label='Depth', ylim=(50, -1.5)) + + +# %% +# Now plot the sea surface height along the transect. +# As the sea surface height does not have a depth axis +# the transect is one dimensional. + +# sphinx_gallery_defer_figures +# sphinx_gallery_capture_repr_block = () + +eta_axes.set_title('Sea surface height') +eta_artist = north_transect.make_artist( + eta_axes, data_array=dataset['eta']) +# xaxis +transect.setup_distance_axis(north_transect, eta_axes) +# yaxis +eta_axes.set_ylim(-0.5, 1.5) +eta_axes.set_ylabel('Height above\nmean sea level') +eta_axes.axhline(0, linestyle='--', color='lightgrey') +eta_axes.yaxis.set_major_formatter("{x:.2g} m") + +# %% +# The last step is to add some landmarks along the top border of the axes +# to help viewers link the distance along transect path to geographic locations. + +top_axis = temp_axes.secondary_xaxis('top') +top_axis.set_ticks( + [north_transect.distance_along_line(point) for label, point in landmarks], + [label for label, point in landmarks], +) -plot.add_coast(axes, zorder=1) -plot.add_gridlines(axes) -plot.add_landmarks(axes, landmarks) -pyplot.show() +plt.show() diff --git a/src/emsarray/transect.py b/src/emsarray/transect.py deleted file mode 100644 index 507f22a..0000000 --- a/src/emsarray/transect.py +++ /dev/null @@ -1,807 +0,0 @@ -import dataclasses -from collections.abc import Callable, Iterable -from functools import cached_property -from typing import Any, cast - -import cfunits -import numpy -import shapely -import xarray -from cartopy import crs -from matplotlib import animation, pyplot -from matplotlib.artist import Artist -from matplotlib.axes import Axes -from matplotlib.collections import PolyCollection -from matplotlib.colors import Colormap -from matplotlib.figure import Figure -from matplotlib.ticker import EngFormatter, Formatter - -from emsarray.conventions import Convention -from emsarray.plot import _requires_plot, make_plot_title -from emsarray.types import DataArrayOrName, Landmark -from emsarray.utils import move_dimensions_to_end, name_to_data_array - -# Useful for calculating distances in a AzimuthalEquidistant projection -# centred on some point: -# -# az = crs.AzimuthalEquidistant(p1.x, p1.y) -# distance = az.project_geometry(p2).distance(ORIGIN) -ORIGIN = shapely.Point(0, 0) - - -def plot( - dataset: xarray.Dataset, - line: shapely.LineString, - data_array: xarray.DataArray, - *, - figsize: tuple = (12, 3), - **kwargs: Any, -) -> Figure: - """ - Plot a transect of a dataset. - - This is convenience function that handles the most common use cases. - For more options refer to the :class:`.Transect` class. - - Parameters - ---------- - dataset : xarray.Dataset - The dataset to transect. - line : shapely.LineString - The transect path to plot. - data_array : xarray.DataArray - A variable from the dataset to plot. - figsize : tuple of int, int - The size of the figure. - **kwargs - Passed to :meth:`Transect.plot_on_figure()`. - """ - figure = pyplot.figure(layout="constrained", figsize=figsize) - depth_coordinate = dataset.ems.get_depth_coordinate_for_data_array(data_array) - transect = Transect(dataset, line, depth=depth_coordinate) - transect.plot_on_figure(figure, data_array, **kwargs) - pyplot.show() - return figure - - -@dataclasses.dataclass -class TransectPoint: - """ - A TransectPoint holds information about each vertex along a transect path. - """ - #: The original point, in the CRS of the line string / dataset. - point: shapely.Point - - #: An AzimuthalEquidistant CRS centred on this point. - crs: crs.AzimuthalEquidistant - - #: The distance in metres of this point along the line. - distance_metres: float - - #: The projected distance along the line of this point. - #: This is normalised to [0, 1]. - #: The actual value is meaningless but can be used to find - #: the closest vertex on the line string for any other projected point. - distance_normalised: float - - -@dataclasses.dataclass -class TransectSegment: - """ - A TransectSegment holds information about each intersecting segment of the - transect path and the dataset cells. - """ - start_point: shapely.Point - end_point: shapely.Point - intersection: shapely.LineString - start_distance: float - end_distance: float - linear_index: int - polygon: shapely.Polygon - - -class Transect: - """ - """ - #: The dataset to plot a transect through - dataset: xarray.Dataset - - #: The transect path to plot - line: shapely.LineString - - #: The depth coordinate (or the name of the depth coordinate) for the dataset. - depth: xarray.DataArray - - def __init__( - self, - dataset: xarray.Dataset, - line: shapely.LineString, - depth: DataArrayOrName | None = None, - ): - self.dataset = dataset - self.convention = dataset.ems - self.line = line - if depth is not None: - self.depth = name_to_data_array(dataset, depth) - else: - self.depth = self.convention.depth_coordinate - - @cached_property - def convention(self) -> Convention: - convention: Convention = self.dataset.ems - return convention - - @cached_property - def transect_dataset(self) -> xarray.Dataset: - """ - A :class:`~xarray.Dataset` containing all the transect geometry. - This includes the depth data, path lengths, - and the linear index of each intersecting cell in the source dataset. - This transect dataset contains all the information necessary to generate a plot, - except for the actual variable data being plotted. - """ - depth = self.depth - - depth_dimension = depth.dims[0] - - depth_bounds = None - try: - depth_bounds = self.convention.dataset[depth.attrs['bounds']].values - except KeyError: - # Make up some depth bounds data from the depth values - # The top/bottom values will be the first/last depth values, - # all other points are the midpoints between the neighbouring points. - depth_midpoints = numpy.concatenate([ - [depth.values[0]], - (depth.values[1:] + depth.values[:-1]) / 2, - [depth.values[-1]] - ]) - depth_bounds = numpy.column_stack(( - depth_midpoints[:-1], - depth_midpoints[1:], - )) - - try: - positive_down = depth.attrs['positive'] == 'down' - except KeyError as err: - raise ValueError( - f'Depth variable {depth.name!r} must have a `positive` attribute' - ) from err - - linear_indexes = [segment.linear_index for segment in self.segments] - depth = xarray.DataArray( - data=depth.values, - dims=(depth_dimension,), - attrs={ - 'bounds': 'depth_bounds', - 'positive': 'down' if positive_down else 'up', - 'long_name': depth.attrs.get('long_name'), - 'description': depth.attrs.get('description'), - 'units': depth.attrs.get('units'), - }, - ) - depth_bounds = xarray.DataArray( - data=depth_bounds, - dims=(depth_dimension, 'bounds'), - ) - distance_bounds = xarray.DataArray( - data=numpy.fromiter( - ( - [segment.start_distance, segment.end_distance] - for segment in self.segments - ), - # Be explicit here, to handle the case when len(self.segments) == 0. - # This happens when the transect line does not intersect the dataset. - # This will result in an empty transect plot. - count=len(self.segments), - dtype=numpy.dtype((float, 2)), - ), - dims=('index', 'bounds'), - attrs={ - 'long_name': 'Distance along transect', - 'units': 'm', - 'start_distance': self.points[0].distance_metres, - 'end_distance': self.points[-1].distance_metres, - }, - ) - linear_index = xarray.DataArray( - data=linear_indexes, - dims=('index',) - ) - - return xarray.Dataset( - data_vars={ - 'depth_bounds': depth_bounds, - 'distance_bounds': distance_bounds, - }, - coords={ - 'depth': depth, - 'linear_index': linear_index, - }, - ) - - def _set_up_axis(self, variable: xarray.DataArray) -> tuple[str, Formatter]: - title = str(variable.attrs.get('long_name')) - units: str | None = variable.attrs.get('units') - - if units is not None: - # Use cfunits to normalize the units to their short symbol form. - # EngFormatter will write 'k{unit}', 'G{unit}', etc - # so unit symbols are required. - formatted_units = cfunits.Units(units).formatted() - formatter = EngFormatter(unit=formatted_units) - - return title, formatter - - def _crs_for_point( - self, - point: shapely.Point, - globe: crs.Globe | None = None, - ) -> crs.Projection: - return crs.AzimuthalEquidistant( - central_longitude=point.x, central_latitude=point.y, globe=globe) - - @cached_property - def points( - self, - ) -> list[TransectPoint]: - """ - A list of :class:`TransectPoints `, - one for each point in the transect :attr:`.line`. - """ - data_crs = self.convention.data_crs - globe = data_crs.globe - - # Make the TransectPoint for the first point by hand. - point = shapely.Point(self.line.coords[0]) - points = [TransectPoint( - point=point, - crs=self._crs_for_point(point, globe), - distance_metres=0, - distance_normalised=0, - )] - - # Make a TransectPoint for each subsequent point along the line. - for point in map(shapely.Point, self.line.coords[1:]): - previous = points[-1] - - # Calculate the distance from the previous point - # by using the AzimuthalEquidistant CRS centred on the previous point. - distance_from_previous = ORIGIN.distance( - previous.crs.project_geometry(point, src_crs=data_crs)) - - points.append(TransectPoint( - point=point, - crs=self._crs_for_point(point, globe), - distance_metres=previous.distance_metres + distance_from_previous, - distance_normalised=self.line.project(point, normalized=True) - )) - - return points - - @cached_property - def segments(self) -> list[TransectSegment]: - """ - A list of :class:`.TransectSegmens` for each intersecting segment of the transect line and the dataset geometry. - Segments are listed in order from the start of the line to the end of the line. - """ - segments = [] - - grid = self.convention.grids[self.convention.default_grid_kind] - polygons = grid.geometry - - # Find all the cell polygons that intersect the line - intersecting_indexes = grid.strtree.query(self.line, predicate='intersects') - - for linear_index in intersecting_indexes: - polygon = polygons[linear_index] - for intersection in self._intersect_polygon(polygon): - # The line will have two ends. - # The intersection starts and ends at these points. - # Project those points alone the original line to find - # the start and end distance of the intersection along the line. - points = [ - shapely.Point(intersection.coords[0]), - shapely.Point(intersection.coords[-1]) - ] - projections: Iterable[tuple[shapely.Point, float]] = ( - (point, self.distance_along_line(point)) - for point in points) - start, end = sorted(projections, key=lambda pair: pair[1]) - - segments.append(TransectSegment( - start_point=start[0], - end_point=end[0], - intersection=intersection, - start_distance=start[1], - end_distance=end[1], - linear_index=linear_index, - polygon=polygon, - )) - - return sorted(segments, key=lambda i: (i.start_distance, i.end_distance)) - - def _intersect_polygon( - self, - polygon: shapely.Polygon, - ) -> list[shapely.LineString]: - """ - Intersect a cell of the dataset geometry with the transect line, - and return a list of all LineString segments of the intersection. - This assumes that the cell does intersect the transect line. - A line and a polygon can intersect in a number of ways: - - * a simple cut through the polygon - * the line starts and/or stops in the polygon - * the line intersects the polygon at a point - * the line intersects the polygon multiple times - - Only the intersections that are line segments are returned. - Multiple intersections (represented as a GeometryCollection) - are decomposed in to the component geometries. - Points are ignored. - - Parameters - ---------- - polygon : shapely.Polygon - The cell geometry to intersect - - Returns - ------- - list of shapely.LineString - All intersecting line strings - """ - intersection = polygon.intersection(self.line) - if isinstance(intersection, (shapely.GeometryCollection, shapely.MultiLineString)): - geoms = intersection.geoms - else: - geoms = [intersection] - return [geom for geom in geoms if isinstance(geom, shapely.LineString)] - - def distance_along_line(self, point: shapely.Point) -> float: - """ - Calculate the distance in metres that the point - falls along the :attr:`transect line <.line>`. - If the point is not on the line, - the point is projected on to the line - and the distance is calculated to this point instead. - - This can be used to calculate the distance along the transect line - to landmark features. - These landmark features can be added as tick points along the transect. - The landmark features need not fall directly on the line. - - Parameters - ---------- - point : shapely.Point - The point to calculate the distance to - - Returns - ------- - float - The distance the point is along the line in meters. - If the point does not fall on the line, - the point is first projected to the line. - """ - data_crs = self.convention.data_crs - distance_normalised = self.line.project(point, normalized=True) - if distance_normalised < 0 or distance_normalised > 1: - raise ValueError("Point is not on the line!") - - # Find the TransectPoint for the vertex before this point on the line - line_point = next( - lp for lp in reversed(self.points) - if lp.distance_normalised <= distance_normalised) - - distance_from_point: float = ORIGIN.distance( - line_point.crs.project_geometry(point, src_crs=data_crs)) - return line_point.distance_metres + distance_from_point - - def make_poly_collection( - self, - **kwargs: Any, - ) -> PolyCollection: - """ - Make a :class:`matplotlib.collections.PolyCollection` - representing the transect geometry. - - Parameters - ---------- - **kwargs - Any keyword arguments are passed to the PolyCollection constructor. - - Returns - ------- - matplotlib.collections.PolyCollection - A PolyCollection representing all the cells - and all the depths the transect line intesected. - """ - transect_dataset = self.transect_dataset - distance_bounds = transect_dataset['distance_bounds'].values - depth_bounds = transect_dataset['depth_bounds'].values - vertices = [ - [ - (distance_bounds[index, 0], depth_bounds[depth_index][0]), - (distance_bounds[index, 0], depth_bounds[depth_index][1]), - (distance_bounds[index, 1], depth_bounds[depth_index][1]), - (distance_bounds[index, 1], depth_bounds[depth_index][0]), - ] - for depth_index in range(transect_dataset.coords['depth'].size) - for index in range(transect_dataset.sizes['index']) - ] - return PolyCollection(vertices, **kwargs) - - def make_ocean_floor_poly_collection( - self, - bathymetry: xarray.DataArray, - **kwargs: Any - ) -> PolyCollection: - """ - Make a :class:`matplotlib.collections.PolyCollection` - representing the ocean floor. - This can be overlayed on a transect plot to mask out values below the sea floor. - - Parameters - ---------- - bathymetry : xarray.Dataset - A data array containing bathymetry data for the dataset. - **kwargs - Any keyword arguments are passed on to the - :class:`~matplotlib.collections.PolyCollection` constructor - - Returns - ------- - matplotlib.collections.PolyCollection - A collection of polygons representing - the ocean floor along the transect path. - """ - transect_dataset = self.transect_dataset - depth = transect_dataset['depth'] - - bathymetry_values = self.convention.ravel(bathymetry) - # The bathymetry data can be oriented differently to the depth coordinate. - # Correct for this if so. - if 'positive' in bathymetry.attrs: - if bathymetry.attrs['positive'] != depth.attrs['positive']: - bathymetry_values = -bathymetry_values - - positive_down = depth.attrs['positive'] == 'down' - deepest_fn = numpy.nanmax if positive_down else numpy.nanmin - deepest = deepest_fn(bathymetry_values.values) - - distance_bounds = transect_dataset['distance_bounds'].values - linear_indexes = transect_dataset['linear_index'].values - - vertices = [ - [ - (distance_bounds[index, 0], bathymetry_values[linear_indexes[index]]), - (distance_bounds[index, 0], deepest), - (distance_bounds[index, 1], deepest), - (distance_bounds[index, 1], bathymetry_values[linear_indexes[index]]), - ] - for index in range(transect_dataset.sizes['index']) - ] - return PolyCollection(vertices, **kwargs) - - def prepare_data_array_for_transect(self, data_array: xarray.DataArray) -> xarray.DataArray: - """ - Prepare a data array for being used as the data in a transect plot. - - Parameters - ---------- - data_array : xarray.DataArray - The data array that will be plotted - - Returns - ------- - xarray.DataArray - The input data array transformed to have the correct shape - for plotting on the transect. - """ - # Some of the following operations drop attrs, - # so keep a reference to the original ones - attrs = data_array.attrs - - data_array = self.convention.ravel(data_array) - - depth_dimension = self.transect_dataset.coords['depth'].dims[0] - index_dimension = data_array.dims[-1] - data_array = move_dimensions_to_end(data_array, [depth_dimension, index_dimension]) - - linear_indexes = self.transect_dataset['linear_index'].values - data_array = data_array.isel({index_dimension: linear_indexes}) - - # Restore attrs after reformatting - data_array.attrs.update(attrs) - - return data_array - - def _find_depth_bounds(self, data_array: xarray.DataArray) -> tuple[int, int]: - """ - Find the shallowest and deepest layers of the data array - where there is at least one value per depth. - - Most ocean models represent cells that are below the sea floor as nans. - Some ocean models do the same for layers above the sea surface, - which can vary due to tides. - If a transect covers mostly shallow regions - but the dataset includes very deep layers - the shallow regions become very small on the final plot. - - This function finds the indexes of the deepest and shallowest layers - where the values are not entirely nan - along the transect path. - The transect plot can use these to only plot depth values that have data, - trimming off layers that are nothing but ocean floor. - """ - transect_dataset = self.transect_dataset - dim = transect_dataset['depth'].dims[0] - - start = 0 - for index in range(transect_dataset['depth'].size): - if numpy.any(numpy.isfinite(data_array.isel({dim: index}).values)): - start = index - break - - stop = -1 - for index in reversed(range(transect_dataset['depth'].size)): - if numpy.any(numpy.isfinite(data_array.isel({dim: index}))): - stop = index - break - - return start, stop - - @_requires_plot - def plot_on_figure( - self, - figure: Figure, - data_array: xarray.DataArray, - *, - title: str | None = None, - trim_nans: bool = True, - clamp_to_surface: bool = True, - bathymetry: xarray.DataArray | None = None, - cmap: str | Colormap | None = None, - clim: tuple[float, float] | None = None, - ocean_floor_colour: str = 'black', - landmarks: list[Landmark] | None = None, - ) -> None: - """ - Plot the data array along this transect. - - Parameters - ---------- - figure : matplotlib.figure.Figure - The figure to plot on - data_array : xarray.DataArray - The data array to plot. - This should be a data array from the dataset provided to the - Transect constructor, - or a data array of compatible shape. - title : str, optional - The title of the plot. - Defaults to the 'long_name' attribute of the data array. - trim_nans : bool, default True - Whether to trim layers containing all nans. - Layers that are entirely under the ocean floor are often represented as nans. - Without trimming, transects through shallow areas mostly look like ocean floor. - clamp_to_surface : bool, default True - If true, clamp the y-axis to 0 m. - Some datasets define an upper depth bound of some large number - which rather spoils the plot. - bathymetry : xarray.DataArray, optional - A data array containing bathymetry information for the dataset. - This will be used to draw a more detailed ocean floor mask. - ocean_floor_colour : str, default 'grey' - The colour to draw the ocean floor in. - This is used to draw cells containing nan values, - and the bathymetry data. - landmarks : list of str, :class:`shapely.Point` tuples - A list of (name, point) tuples. - These will be added as tick marks along the top of the plot. - """ - axes, collection, data_array = self._plot_on_figure( - figure=figure, - data_array=data_array, - title=title, - trim_nans=trim_nans, - clamp_to_surface=clamp_to_surface, - bathymetry=bathymetry, - cmap=cmap, - clim=clim, - ocean_floor_colour=ocean_floor_colour, - landmarks=landmarks, - ) - collection.set_array(data_array.values.flatten()) - - def animate_on_figure( - self, - figure: Figure, - data_array: xarray.DataArray, - *, - title: str | Callable[[Any], str] | None = None, - trim_nans: bool = True, - clamp_to_surface: bool = True, - bathymetry: xarray.DataArray | None = None, - cmap: str | Colormap | None = None, - clim: tuple[float, float] | None = None, - ocean_floor_colour: str = 'black', - landmarks: list[Landmark] | None = None, - coordinate: xarray.DataArray | None = None, - interval: int = 200, - ) -> animation.FuncAnimation: - """ - Plot the data array along this transect. - - Parameters - ---------- - figure : matplotlib.figure.Figure - The figure to plot on - data_array : xarray.DataArray - The data array to plot. - This should be a data array from the dataset provided to the - Transect constructor, - or a data array of compatible shape. - title : str or callable - The title of the plot. - coordinate : xarray.DataArray - The coordinate to animate along. - Defaults to the time coordinate. - interval : int - Time in milliseconds between frames. - **kwargs - See :meth:`.plot_on_figure` for available keyword arguments - """ - if coordinate is None: - coordinate = self.convention.time_coordinate - coordinate_indexes = numpy.arange(coordinate.size) - animation_dimension = coordinate.dims[0] - - coordinate_callable: Callable[[Any], str] - if title is None: - title = data_array.attrs.get('long_name') - if title is not None: - coordinate_callable = lambda c: f'{title}\n{c}' - else: - coordinate_callable = str - - elif isinstance(title, str): - coordinate_callable = title.format - - else: - coordinate_callable = title - - first_frame = data_array.isel({animation_dimension: 0}) - first_frame.load() - axes, collection, _prepared_frame = self._plot_on_figure( - figure=figure, - data_array=first_frame, - title=None, - trim_nans=trim_nans, - clamp_to_surface=clamp_to_surface, - bathymetry=bathymetry, - cmap=cmap, - clim=clim, - ocean_floor_colour=ocean_floor_colour, - landmarks=landmarks, - ) - - def animate(index: int) -> Iterable[Artist]: - changes: list[Artist] = [] - - coordinate_value = coordinate.values[index] - axes.set_title(coordinate_callable(coordinate_value)) - changes.append(axes) - - frame_data = data_array.isel({animation_dimension: index}) - frame_data.load() - prepared_data = self.prepare_data_array_for_transect(frame_data) - collection.set_array(prepared_data.values.flatten()) - changes.append(collection) - return changes - - # Draw the figure to force everything to compute its size - figure.draw_without_rendering() - - # Set the first frame of data - animate(0) - - # Make the animation - return animation.FuncAnimation( - figure, animate, frames=coordinate_indexes, - interval=interval) - - def _plot_on_figure( - self, - figure: Figure, - data_array: xarray.DataArray, - *, - title: str | None = None, - trim_nans: bool = True, - clamp_to_surface: bool = True, - bathymetry: xarray.DataArray | None = None, - cmap: str | Colormap | None = None, - clim: tuple[float, float] | None = None, - ocean_floor_colour: str = 'black', - landmarks: list[Landmark] | None = None, - ) -> tuple[Axes, PolyCollection, xarray.DataArray]: - """ - Construct the axes and PolyCollections on a plot, - and reformat the data array to the correct shape for plotting. - Assigning the data is left to the caller, - to support both static and animated plots. - """ - transect_dataset = self.transect_dataset - depth = transect_dataset.coords['depth'] - distance_bounds = transect_dataset.data_vars['distance_bounds'] - - data_array = data_array.load() - data_array = self.prepare_data_array_for_transect(data_array) - - positive_down = depth.attrs['positive'] == 'down' - d1, d2 = depth.values[0:2] - deep_to_shallow = (d1 > d2) == positive_down - - if trim_nans: - depth_start, depth_stop = self._find_depth_bounds(data_array) - else: - depth_start, depth_stop = 0, -1 - if deep_to_shallow: - depth_start, depth_stop = depth_stop, depth_start - - down, up = ( - (numpy.nanmax, numpy.nanmin) - if positive_down - else (numpy.nanmin, numpy.nanmax)) - if clamp_to_surface: - depth_limit_shallow = 0 - else: - depth_limit_shallow = up(transect_dataset['depth_bounds'][depth_start]) - depth_limit_deep = down(transect_dataset['depth_bounds'][depth_stop]) - - axes = cast(Axes, figure.subplots()) - x_title, x_formatter = self._set_up_axis(distance_bounds) - y_title, y_formatter = self._set_up_axis(depth) - axes.set_xlabel(x_title) - axes.set_ylabel(y_title) - axes.xaxis.set_major_formatter(x_formatter) - axes.yaxis.set_major_formatter(y_formatter) - axes.set_xlim( - distance_bounds.attrs['start_distance'], - distance_bounds.attrs['end_distance'], - ) - axes.set_ylim(depth_limit_deep, depth_limit_shallow) - - if title is None: - title = make_plot_title(self.dataset, data_array) - if title is not None: - axes.set_title(title) - - cmap = pyplot.get_cmap(cmap).copy() - cmap.set_bad(ocean_floor_colour) - - # Find a min/max from the data if clim isn't provided and the data array is not empty. - # An empty data array happens when the transect line does not intersect - # the dataset geometry. - if clim is None and data_array.size != 0: - clim = (numpy.nanmin(data_array), numpy.nanmax(data_array)) - - collection = self.make_poly_collection(cmap=cmap, clim=clim, edgecolor='face') - axes.add_collection(collection) - - if bathymetry is not None: - ocean_floor = self.make_ocean_floor_poly_collection( - bathymetry, facecolor=ocean_floor_colour) - axes.add_collection(ocean_floor) - - units = data_array.attrs.get('units') - figure.colorbar(collection, ax=axes, location='right', label=units) - - if landmarks is not None: - top_axis = axes.secondary_xaxis('top') - top_axis.set_ticks( - [self.distance_along_line(point) for label, point in landmarks], - [label for label, point in landmarks], - ) - - return axes, collection, data_array diff --git a/src/emsarray/transect/__init__.py b/src/emsarray/transect/__init__.py new file mode 100644 index 0000000..0e91cca --- /dev/null +++ b/src/emsarray/transect/__init__.py @@ -0,0 +1,8 @@ +from .base import Transect, TransectPoint, TransectSegment +from .utils import plot, setup_depth_axis, setup_distance_axis + + +__all__ = [ + 'Transect', 'TransectPoint', 'TransectSegment', + 'plot', 'setup_depth_axis', 'setup_distance_axis', +] diff --git a/src/emsarray/transect/artists.py b/src/emsarray/transect/artists.py new file mode 100644 index 0000000..13bc62e --- /dev/null +++ b/src/emsarray/transect/artists.py @@ -0,0 +1,130 @@ +from typing import Any + +import numpy +import xarray +from matplotlib.artist import Artist +from matplotlib.collections import QuadMesh +from matplotlib.patches import StepPatch + +from . import base + + +class TransectArtist(Artist): + """ + A matplotlib Artist subclass that knows what Transect it is associated with, + and has a `set_data_array()` method. + Users can call `TransectArtist.set_data_array()` to update the data in a plot. + This is useful when making animations, for example. + """ + _transect: 'base.Transect' + + def set_transect(self, transect: 'base.Transect') -> None: + if hasattr(self, '_transect'): + raise ValueError("_transect can not be changed once set") + self._transect = transect + + def get_transect(self) -> 'base.Transect': + return self._transect + + def set_data_array(self, data_array: Any) -> None: + """ + Update the data this artist is plotting. + """ + raise NotImplementedError("Subclasses must implement this") + + +class CrossSectionArtist(QuadMesh, TransectArtist): + @classmethod + def from_transect( + cls, + transect: "base.Transect", + *, + data_array: xarray.DataArray | None = None, + depth_coordinate: xarray.DataArray | None = None, + **kwargs: Any, + ) -> "CrossSectionArtist": + """ + Construct a :class:`CrossSectionArtist` for a transect. + """ + distance_bounds = transect.intersection_bounds + + if depth_coordinate is None and data_array is None: + raise ValueError( + "At least one of data_array and depth_coordinate must be not None") + if depth_coordinate is None: + depth_coordinate = transect.convention.get_depth_coordinate_for_data_array(data_array) + depth_bounds = transect.dataset[depth_coordinate.attrs['bounds']].values + + holes = transect.holes + xs = numpy.concat([distance_bounds[:, 0], distance_bounds[-1:, 1]]) + xs = numpy.insert(xs, holes, distance_bounds[holes - 1, 1]) + ys = numpy.concat([depth_bounds[:, 0], depth_bounds[-1:, 1]]) + coordinates = numpy.stack(numpy.meshgrid(xs, ys), axis=-1) + + # There are issues with passing both transect and data array to the constructor + # where the `set_data_array()` is called before `set_transect()`. + # Doing it this way is safe but kinda gross. + artist = cls(coordinates, transect=transect, **kwargs) + if data_array is not None: + artist.set_data_array(data_array) + + return artist + + def set_data_array(self, data_array: xarray.DataArray) -> None: + if len(self._transect.segments) > 0: + self.set_array(self.prepare_data_array(self._transect, data_array)) + + @staticmethod + def prepare_data_array(transect: "base.Transect", data_array: xarray.DataArray) -> numpy.ndarray: + values = transect.extract(data_array).values + values = numpy.insert(values, transect.holes, numpy.nan, axis=-1) + return values + + +class TransectStepArtist(StepPatch, TransectArtist): + _edge_default = True + + @classmethod + def from_transect( + cls, + transect: "base.Transect", + *, + data_array: xarray.DataArray | None = None, + **kwargs: Any, + ) -> "TransectStepArtist": + """ + Construct a :class:`TransectStepArtist` for a transect. + """ + holes = transect.holes + x_bounds = transect.intersection_bounds + if len(transect.segments) == 0: + edges = numpy.array([0.]) + else: + edges = x_bounds[:, 0] + edges = numpy.append(edges, x_bounds[-1, 1]) + edges = numpy.insert(edges, holes, x_bounds[holes - 1, 1]) + + if data_array is not None: + values = cls.prepare_data_array(transect, data_array) + else: + values = numpy.full(shape=(len(edges) - 1,), fill_value=numpy.nan) + + return cls(values, edges, transect=transect, **kwargs) + + def set_data_array(self, data_array: xarray.DataArray) -> None: + self.set_data(self.prepare_data_array(self._transect, data_array)) + + @staticmethod + def prepare_data_array(transect: "base.Transect", data_array: xarray.DataArray) -> numpy.ndarray: + values = transect.extract(data_array).values + assert len(values.shape) == 1 + + # If a transect path is not fully contained within the dataset geometry + # the path will have gaps. We can represent these gaps using nans. + values = numpy.insert( + values.astype(float), # Upcast to float in case this was an integer array + transect.holes, + numpy.nan, + ) + + return values diff --git a/src/emsarray/transect/base.py b/src/emsarray/transect/base.py new file mode 100644 index 0000000..887c480 --- /dev/null +++ b/src/emsarray/transect/base.py @@ -0,0 +1,609 @@ +import dataclasses +from collections.abc import Iterable +from functools import cached_property +from typing import Any, cast + +import numpy +import shapely +import xarray +from cartopy import crs +from matplotlib.axes import Axes +from matplotlib.typing import ColorType + +from emsarray.conventions import Convention, Grid +from emsarray.exceptions import NoSuchCoordinateError +from emsarray.types import DataArrayOrName +from emsarray.utils import move_dimensions_to_end, name_to_data_array + +from . import artists + + +# Useful for calculating distances in a AzimuthalEquidistant projection +# centred on some point: +# +# az = crs.AzimuthalEquidistant(p1.x, p1.y) +# distance = az.project_geometry(p2).distance(ORIGIN) +ORIGIN = shapely.Point(0, 0) + + +@dataclasses.dataclass +class TransectPoint: + """ + A TransectPoint holds information about each vertex along a transect path. + """ + #: The original point, in the CRS of the line string / dataset. + point: shapely.Point + + #: An AzimuthalEquidistant CRS centred on this point. + crs: crs.AzimuthalEquidistant + + #: The distance in metres of this point along the line. + distance_metres: float + + #: The projected distance along the line of this point. + #: This is normalised to [0, 1]. + #: The actual value is meaningless but can be used to find + #: the closest vertex on the line string for any other projected point. + distance_normalised: float + + +@dataclasses.dataclass +class TransectSegment: + """ + A TransectSegment holds information about each intersecting segment of the + transect path and the dataset cells. + """ + #: The point where the transect path first intersects this dataset cell + start_point: shapely.Point + #: The point where the transect exits this dataset cell + end_point: shapely.Point + #: The entire intersection between the transect path and this dataset cell + intersection: shapely.LineString + #: The distance along the line in metres to the :attr:`.start_point` + start_distance: float + #: The distance along the line in metres to the :attr:`.end_point` + end_distance: float + #: The linear index of this dataset cell + linear_index: int + #: The polygon of the dataset cell + polygon: shapely.Polygon + + +class Transect: + """ + """ + #: The dataset to plot a transect through + dataset: xarray.Dataset + + #: The transect path to plot + line: shapely.LineString + + #: The dataset grid to transect. + grid: Grid + + def __init__( + self, + dataset: xarray.Dataset, + line: shapely.LineString, + *, + grid: Grid | None = None, + ): + self.dataset = dataset + self.convention = cast(Convention, dataset.ems) + self.line = line + if grid is None: + grid = self.convention.default_grid + self.grid = grid + + @cached_property + def intersection_bounds( + self, + ) -> numpy.ndarray: + """ + A numpy array of shape `(len(segments), 2)` + indicating the distance to the start and end of each intersection segment. + This is a shortcut to :attr:`TransectSegment.start_distance` + and :attr:`~TransectSegment.end_distance` from :attr:`Transect.segments`. + """ + return numpy.fromiter( + ( + [segment.start_distance, segment.end_distance] + for segment in self.segments + ), + # Be explicit here, to handle the case when len(self.segments) == 0. + # This happens when the transect line does not intersect the dataset. + # This will result in an empty transect plot. + count=len(self.segments), + dtype=numpy.dtype((float, 2)), + ) + + @cached_property + def linear_indexes(self) -> numpy.ndarray: + """ + A numpy array of length `len(segments)` + of the linear indexes of each intersecting polygon, in order. + This is a shortcut to :attr:`TransectSegment.linear_index` + from :attr:`Transect.segments`. + """ + return numpy.fromiter( + (segment.linear_index for segment in self.segments), + count=len(self.segments), + dtype=numpy.dtype(int), + ) + + @cached_property + def holes(self) -> numpy.ndarray: + """ + An array with the index of any discontinuities in the transect segments. + For transect paths that are entirely within the dataset geometry this will be empty. + For paths that pass in and out of the dataset geometry + this will be the index of the segment just after the discontinuity. + Two segments are not contiguous if `segment[n].end_distance != segment[n+1].start_distance` + """ + bounds = self.intersection_bounds + return numpy.flatnonzero(bounds[:-1, 1] != bounds[1:, 0]) + 1 + + def _crs_for_point( + self, + point: shapely.Point, + globe: crs.Globe | None = None, + ) -> crs.Projection: + return crs.AzimuthalEquidistant( + central_longitude=point.x, central_latitude=point.y, globe=globe) + + @cached_property + def points( + self, + ) -> list[TransectPoint]: + """ + A list of :class:`TransectPoints `, + one for each point in the transect :attr:`.line`. + """ + data_crs = self.convention.data_crs + globe = data_crs.globe + + # Make the TransectPoint for the first point by hand. + point = shapely.Point(self.line.coords[0]) + points = [TransectPoint( + point=point, + crs=self._crs_for_point(point, globe), + distance_metres=0, + distance_normalised=0, + )] + + # Make a TransectPoint for each subsequent point along the line. + for point in map(shapely.Point, self.line.coords[1:]): + previous = points[-1] + + # Calculate the distance from the previous point + # by using the AzimuthalEquidistant CRS centred on the previous point. + distance_from_previous = ORIGIN.distance( + previous.crs.project_geometry(point, src_crs=data_crs)) + + points.append(TransectPoint( + point=point, + crs=self._crs_for_point(point, globe), + distance_metres=previous.distance_metres + distance_from_previous, + distance_normalised=self.line.project(point, normalized=True) + )) + + return points + + @cached_property + def segments(self) -> list[TransectSegment]: + """ + A list of :class:`TransectSegments <.TransectSegment>` + for each intersecting segment of the transect line and the dataset geometry. + Segments are listed in order from the start of the line to the end of the line. + """ + segments = [] + + grid = self.convention.grids[self.convention.default_grid_kind] + polygons = grid.geometry + + # Find all the cell polygons that intersect the line + intersecting_indexes = grid.strtree.query(self.line, predicate='intersects') + + for linear_index in intersecting_indexes: + polygon = polygons[linear_index] + for intersection in self._intersect_polygon(polygon): + # The line will have two ends. + # The intersection starts and ends at these points. + # Project those points alone the original line to find + # the start and end distance of the intersection along the line. + points = [ + shapely.Point(intersection.coords[0]), + shapely.Point(intersection.coords[-1]) + ] + projections: Iterable[tuple[shapely.Point, float]] = ( + (point, self.distance_along_line(point)) + for point in points) + start, end = sorted(projections, key=lambda pair: pair[1]) + + segments.append(TransectSegment( + start_point=start[0], + end_point=end[0], + intersection=intersection, + start_distance=start[1], + end_distance=end[1], + linear_index=linear_index, + polygon=polygon, + )) + + return sorted(segments, key=lambda i: (i.start_distance, i.end_distance)) + + @cached_property + def coordinates(self) -> xarray.Dataset: + """ + A :class:`xarray.Dataset` containing coordinate information + for data extracted along the transect. + """ + index_dim = 'index' + coordinates = xarray.Dataset( + coords={ + 'distance': xarray.DataArray( + data=numpy.average(self.intersection_bounds, axis=1), + dims=index_dim, + attrs={ + 'long_name': 'Distance along transect', + 'units': 'm', + 'bounds': 'distance_bounds', + }, + ), + 'distance_bounds': xarray.DataArray( + data=self.intersection_bounds, + dims=(index_dim, 'Two'), + ), + } + ) + return coordinates + + def _intersect_polygon( + self, + polygon: shapely.Polygon, + ) -> list[shapely.LineString]: + """ + Intersect a cell of the dataset geometry with the transect line, + and return a list of all LineString segments of the intersection. + This assumes that the cell does intersect the transect line. + A line and a polygon can intersect in a number of ways: + + * a simple cut through the polygon + * the line starts and/or stops in the polygon + * the line intersects the polygon at a point + * the line intersects the polygon multiple times + + Only the intersections that are line segments are returned. + Multiple intersections (represented as a GeometryCollection) + are decomposed in to the component geometries. + Points are ignored. + + Parameters + ---------- + polygon : shapely.Polygon + The cell geometry to intersect + + Returns + ------- + list of shapely.LineString + All intersecting line strings + """ + intersection = polygon.intersection(self.line) + if isinstance(intersection, (shapely.GeometryCollection, shapely.MultiLineString)): + geoms = intersection.geoms + else: + geoms = [intersection] + return [geom for geom in geoms if isinstance(geom, shapely.LineString)] + + def distance_along_line(self, point: shapely.Point) -> float: + """ + Calculate the distance in metres that the point + falls along the :attr:`transect line <.line>`. + If the point is not on the line, + the point is projected on to the line + and the distance is calculated to this point instead. + + This can be used to calculate the distance along the transect line + to landmark features. + These landmark features can be added as tick points along the transect. + The landmark features need not fall directly on the line. + + Parameters + ---------- + point : shapely.Point + The point to calculate the distance to + + Returns + ------- + float + The distance the point is along the line in meters. + If the point does not fall on the line, + the point is first projected to the line. + """ + data_crs = self.convention.data_crs + distance_normalised = self.line.project(point, normalized=True) + if distance_normalised < 0 or distance_normalised > 1: + raise ValueError("Point is not on the line!") + + # Find the TransectPoint for the vertex before this point on the line + line_point = next( + lp for lp in reversed(self.points) + if lp.distance_normalised <= distance_normalised) + + distance_from_point: float = ORIGIN.distance( + line_point.crs.project_geometry(point, src_crs=data_crs)) + return line_point.distance_metres + distance_from_point + + def extract(self, data_array: DataArrayOrName) -> xarray.DataArray: + """ + Extract data from a data array along a transect. + + Parameters + ---------- + data_array : DataArrayOrName + The data array to extract data from. + + Returns + ------- + xarray.DataArray + A new :class:`xarray.DataArray` containing data from the input data array + extracted along the path of the transect. + """ + data_array = name_to_data_array(self.dataset, data_array) + + # Some of the following operations drop attrs, + # so keep a reference to the original ones + attrs = data_array.attrs + + data_array = self.convention.ravel(data_array) + + index_dimension = data_array.dims[-1] + data_array = move_dimensions_to_end(data_array, [index_dimension]) + + data_array = data_array.isel({index_dimension: self.linear_indexes}) + + # Restore attrs after reformatting + data_array.attrs.update(attrs) + + return data_array + + def make_artist( + self, + axes: Axes, + data_array: DataArrayOrName, + **kwargs: Any, + ) -> 'artists.TransectArtist': + """ + Make an artist to plot values extracted from a data array along this transect. + The kind of artist used depends on the dimensionality of the data array. + + To be plotted along a transect the data array must be defined on a supported :ref:`grid `. + Currently only polygonal grids are supported. + + If a data array has a depth axis, :meth:`.make_cross_section_artist` is called, + otherwise :meth:`.make_transect_step_artist` is called. + + Parameters + ========== + axes : Axes + The :class:`matplotlib.axes.Axes` to add this artist to. + data_array : DataArrayOrName + The data array to plot + **kwargs + Passed on to the artist, can be used to customise the plot style. + + Returns + ======= + :class:`.artists.TransectArtist` + The artist that will plot the data. + This artist will already have been added to the axes. + + See also + ======== + :func:`~.utils.setup_distance_axis` + Setup the x-axis of an :class:`~matplotlib.axes.Axes` + for plotting distance along a transect. + :func:`~.utils.setup_depth_axis` + Setup the y-axis of an :class:`~matplotlib.axes.Axes` + for plotting down a depth coordinate. + """ + data_array = name_to_data_array(self.dataset, data_array) + grid = self.convention.get_grid(data_array) + try: + depth_coordinate = self.convention.get_depth_coordinate_for_data_array(data_array) + except NoSuchCoordinateError: + depth_coordinate = None + + if grid.geometry_type is not shapely.Polygon: + raise ValueError( + f"I don't know how to plot transects across {grid.geometry_type.__name__} geometry.") + + if depth_coordinate is not None: + return self.make_cross_section_artist(axes, data_array, **kwargs) + else: + return self.make_transect_step_artist(axes, data_array, **kwargs) + + def make_cross_section_artist( + self, + axes: Axes, + data_array: DataArrayOrName, + colorbar: bool = True, + **kwargs: Any, + ) -> 'artists.CrossSectionArtist': + """ + Make an artist that plots a vertical slice along the length of the transect. + The data must be three dimensional with a depth axis. + The data are plotted as a grid of values, + with distance along the transect as the x-axis + and depth represented as the y-axis. + + Parameters + ========== + axes : Axes + The :class:`matplotlib.axes.Axes` to add this line to. + data_array : DataArrayOrName + The data array to plot. + This data array must be defined on a polygonal :class:`~emsarray.conventions.Grid` + and must have a depth coordinate with bounds. + colorbar : bool, default True + Whether to add a colorbar for this artist. + Sensible defaults are used for the colorbar, but if more customisation is required + set `colorbar=False` and configure a colorbar manually. + edgecolor : color, optional + The colour of the line. + Optional, defaults to the next available colour in the matplotlib plot colours. + fill : bool, default False + Whether to fill in values between the line and the baseline. + Defaults to False. + **kwargs + Passed on to the :class:`~.artists.TransectStepArtist`, + can be used to customise the plot. + + See also + ======== + :func:`~.utils.setup_distance_axis` + Setup the x-axis of an :class:`~matplotlib.axes.Axes` + for plotting distance along a transect. + :func:`~.utils.setup_depth_axis` + Setup the y-axis of an :class:`~matplotlib.axes.Axes` + for plotting down a depth coordinate. + :func:`~emsarray.utils.estimate_bounds_1d` + Estimate some bounds for a coordinate variable. + """ + data_array = name_to_data_array(self.dataset, data_array) + artist = artists.CrossSectionArtist.from_transect( + self, data_array=data_array, + **kwargs) + axes.add_artist(artist) + if colorbar: + units = data_array.attrs.get('units', None) + axes.figure.colorbar(artist, label=units) + return artist + + def make_transect_step_artist( + self, + axes: Axes, + data_array: DataArrayOrName, + edgecolor: ColorType | None = 'auto', + fill: bool = False, + **kwargs: Any, + ) -> 'artists.TransectStepArtist': + """ + Make an artist that plots values along the length of the transect. + The data must be two dimensional - it must have no depth axis. + The data are plotted as a stepped line. + + Parameters + ========== + axes : Axes + The :class:`matplotlib.axes.Axes` to add this line to. + data_array : DataArrayOrName + The data array to plot. + This data array must be defined on a polygonal :class:`~emsarray.conventions.Grid` + and must not have any other dimensions such as time or depth. + edgecolor : color, optional + The colour of the line. + Optional, defaults to the next available colour in the matplotlib plot colours. + fill : bool, default False + Whether to fill in values between the line and the baseline. + Defaults to False. + **kwargs + Passed on to the :class:`~.artists.TransectStepArtist`, + can be used to customise the plot. + + Returns + ======= + :class:`~.artists.TransectStepArtist` + The artist that will plot the data. + This artist will already have been added to the axes. + + See also + ======== + :func:`.utils.setup_distance_axis` + Setup the x-axis of an :class:`~matplotlib.axes.Axes` + for plotting distance along a transect. + """ + data_array = name_to_data_array(self.dataset, data_array) + if edgecolor == 'auto': + edgecolor = axes._get_lines.get_next_color() + artist = artists.TransectStepArtist.from_transect( + self, data_array=data_array, + fill=fill, edgecolor=edgecolor, **kwargs) + axes.add_artist(artist) + return artist + + def make_ocean_floor_artist( + self, + axes: Axes, + data_array: DataArrayOrName, + fill: bool = True, + facecolor: ColorType | None = 'lightgrey', + edgecolor: ColorType | None = 'none', + baseline: float | None = None, + **kwargs: Any, + ) -> 'artists.TransectStepArtist': + """ + Make an artist that renders a solid polygon following a bathymetry variable. + This can be drawn in front of a cross section artist to mask out values below the ocean floor. + + Parameters + ========== + axes : Axes + The :class:`matplotlib.axes.Axes` to add the ocean floor artist to + data_array : DataArrayOrName + The data array or name of a data array with the ocean floor data + baseline : float, optional + The deepest part of the ocean floor to render. + The ocean floor will be filled in from the bathymetry value down to the baseline. + Optional, if not provided the deepest value in the data array is used instead. + **kwargs + Passed on to the :class:`.artists.TransectStepArtist` for styling. + Set `facecolor` to change the colour of the ocean floor polygon. + + Returns + ======= + .artists.TransectStepArtist + The artist that will render the ocean floor. + This artist will already have been added to the axes. + + Notes + ===== + The `sign convention `_ + of the bathymetry variable and the depth coordinate must match. + If they differ the ocean floor polygon is likely to be either + entirely outside of the plot extent or to cover the entire plot extent. + :func:`~emsarray.operations.depth.normalize_depth_variables` + can be used to change the sign convention of a depth coordinate variable. + + See also + ======== + `CF Conventions on Vertical Coordinates `_ + More information on the `positive` attribute. + :func:`emsarray.operations.depth.normalize_depth_variables` + Update the sign convention of a depth coordinate variable. + + .. _CF-vertical-coordinates: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#vertical-coordinate + + """ + data_array = name_to_data_array(self.dataset, data_array) + if baseline is None: + data_min, data_max = numpy.nanmin(data_array.values), numpy.nanmax(data_array.values) + if 'positive' in data_array.attrs: + if data_array.attrs['positive'] == 'down': + baseline = data_max + else: + baseline = data_min + else: + # Take a guess by using the most extreme value + if numpy.abs(data_min) < numpy.abs(data_max): + baseline = data_max + else: + baseline = data_min + + artist = artists.TransectStepArtist.from_transect( + self, data_array=data_array, + fill=fill, baseline=baseline, + facecolor=facecolor, edgecolor=edgecolor, + **kwargs) + axes.add_artist(artist) + return artist diff --git a/src/emsarray/transect/utils.py b/src/emsarray/transect/utils.py new file mode 100644 index 0000000..d0e84e7 --- /dev/null +++ b/src/emsarray/transect/utils.py @@ -0,0 +1,194 @@ +from typing import Any + +import cfunits +import numpy +import shapely +import xarray +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.figure import Figure +from matplotlib.ticker import EngFormatter + +from emsarray.exceptions import NoSuchCoordinateError +from emsarray.plot import make_plot_title +from emsarray.types import DataArrayOrName, Landmark +from emsarray.utils import name_to_data_array +from .base import Transect + + +def plot( + dataset: xarray.Dataset, + line: shapely.LineString, + data_array: DataArrayOrName, + *, + figsize: tuple = (12, 3), + title: str | None = None, + bathymetry: xarray.DataArray | None = None, + landmarks: list[Landmark] | None = None, + **kwargs: Any, +) -> Figure: + """ + Display a transect or cross section of a dataset along a path. + + This is convenience function that handles the most common use cases. + For more control over the figure, + see the comprehensive :ref:`transect example `. + + Parameters + ---------- + dataset : xarray.Dataset + The dataset to transect. + line : shapely.LineString + The transect path to plot. + data_array : DataArrayOrName + A variable from the dataset to plot. + figsize : tuple of int, int + The size of the figure. + title : str, optional + The title of the plot. + If `None` a title is pulled from the data array + using :func:`~emsarray.plot.make_plot_title`. + bathymetry : DataArrayOrName + Used to draw an ocean floor polygon over the cross section. + Only used if the data array to plot has a depth dimension. + landmarks : list of Landmark + Landmarks to add to the top axis of the plot. + These can help viewers locate the transect in space. + **kwargs + Passed to :meth:`Transect.make_artist()`. + """ + transect = Transect(dataset, line) + + figure = plt.figure(figsize=figsize, layout='constrained') + data_array = name_to_data_array(dataset, data_array) + transect_data = transect.extract(data_array) + try: + depth_coordinate = dataset.ems.get_depth_coordinate_for_data_array(data_array) + except NoSuchCoordinateError: + depth_coordinate = None + + axes = figure.add_subplot() + transect.make_artist(axes, data_array, **kwargs) + + if title is None: + title = make_plot_title(dataset, data_array) + if title: + axes.set_title(title) + + setup_distance_axis(transect, axes) + if depth_coordinate is not None: + if len(transect.segments) > 0: + depths_with_data = numpy.flatnonzero(numpy.isfinite(transect_data.values).any(axis=-1)) + depth_bounds = dataset[depth_coordinate.attrs['bounds']] + ylim = ( + depth_bounds.values[depths_with_data[0], 0], + depth_bounds.values[depths_with_data[-1], 1], + ) + else: + ylim = None + setup_depth_axis(transect, axes, depth_coordinate=depth_coordinate, ylim=ylim) + + if bathymetry is not None: + transect.make_ocean_floor_artist(axes, bathymetry) + + else: + ylim = (numpy.nanmin(transect_data), numpy.nanmax(transect_data)) + axes.set_ylim(ylim) + + if landmarks is not None: + top_axis = axes.secondary_xaxis('top') + top_axis.set_ticks( + [transect.distance_along_line(point) for label, point in landmarks], + [label for label, point in landmarks], + ) + + plt.show() + return figure + + +def setup_distance_axis(transect: Transect, axes: Axes) -> None: + """ + Configure the x-axis of a :class:`~matplotlib.axes.Axes` for values along a transect. + + Parameters + ========== + transect : emsarray.transect.Transect + The transect being plotted + axes : matplotlib.axes.Axes + The axes to configure + """ + axis = axes.xaxis + + axes.set_xlim(transect.points[0].distance_metres, transect.points[-1].distance_metres) + axis.set_label_text("Distance along transect") + axis.set_major_formatter(EngFormatter(unit='m')) + + +def setup_depth_axis( + transect: "Transect", + axes: Axes, + data_array: DataArrayOrName | None = None, + depth_coordinate: DataArrayOrName | None = None, + ylim: tuple[float, float] | bool = True, + label: str | None | bool = True, + units: str | None | bool = True, +) -> None: + """ + Configure the y-axis of a :class:`~matplolib.axes.Axes` for values along a depth coordinate. + + Parameters + ========== + transect : emsarray.transect.Transect + The transect being plotted + axes : matplotlib.axes.Axes + The axes to configure + data_array : DataArrayOrName, optional + depth_coordinate : DataArrayOrName, optional + One of `data_array` or `depth_coordinate` must be provided. + The y-axis is configured to show values along this depth coordinate. + If data_array is provided, the depth coordinate for this data array is used. + ylim : tuple of float, float, optional + The ylim of the axes. If not provided the limit is calculated from the depth coordinate. + label : str or None, optional + The label for the y-axis. + Optional, defaults to the `long_name` attribute of the depth coordinate. + Set to `None` to disable the label. + units : str or None, optional + The units for the y-axis. + Optional, defaults to the `units` attribute of the depth coordinate. + Set to `None` to disable the units and formatting of tick labels. + """ + if data_array is None and depth_coordinate is None: + raise ValueError("Either data_array or depth_coordinate must be provided") + if data_array is not None and depth_coordinate is not None: + raise ValueError("Only one of data_array or depth_bounds must be provided") + + if data_array is not None: + depth_coordinate = transect.convention.get_depth_coordinate_for_data_array(data_array) + else: + depth_coordinate = name_to_data_array(transect.dataset, depth_coordinate) + + axis = axes.yaxis + + if ylim is True: + depth_bounds = transect.dataset[depth_coordinate.attrs['bounds']].values + positive_down = depth_coordinate.attrs['positive'].lower() == 'down' + depth_min, depth_max = numpy.nanmin(depth_bounds), numpy.nanmax(depth_bounds) + + if positive_down: + axes.set_ylim(depth_max, depth_min) + else: + axes.set_ylim(depth_min, depth_max) + elif ylim not in {False, None}: + axes.set_ylim(ylim) + + if label is True: + label = depth_coordinate.attrs.get('long_name') + if label not in {False, None}: + axis.set_label_text(label) + + if units is True: + units = depth_coordinate.attrs.get('units') + if units not in {False, None}: + formatted_units = cfunits.Units(units).formatted() + axis.set_major_formatter(EngFormatter(unit=formatted_units)) diff --git a/tests/test_transect.py b/tests/transect/test_plot.py similarity index 92% rename from tests/test_transect.py rename to tests/transect/test_plot.py index 7993cad..863cb4a 100644 --- a/tests/test_transect.py +++ b/tests/transect/test_plot.py @@ -6,10 +6,14 @@ import shapely import emsarray.transect +from emsarray.utils import estimate_bounds_1d logger = logging.getLogger(__name__) +# Most of the transect code is exercised by the plot examples in the docs. + + @pytest.mark.mpl_image_compare @pytest.mark.matplotlib @pytest.mark.tutorial @@ -18,6 +22,7 @@ def test_plot( tmp_path: pathlib.Path, ): dataset = emsarray.tutorial.open_dataset('gbr4') + dataset = estimate_bounds_1d(dataset, 'zc') temp = dataset['temp'].copy() temp = temp.isel(time=-1) @@ -69,6 +74,8 @@ def test_plot_no_intersection( This should produce an empty transect plot, which is better than raising an error. """ dataset = emsarray.tutorial.open_dataset('gbr4') + dataset = estimate_bounds_1d(dataset, 'zc') + temp = dataset['temp'].copy() temp = temp.isel(time=-1) From ea40d7293f33a47faec745f0dd47c1a3802dea8f Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Thu, 26 Mar 2026 11:50:49 +1100 Subject: [PATCH 2/3] Update transect plot tests --- src/emsarray/transect/artists.py | 22 ++-- src/emsarray/transect/utils.py | 6 +- .../tests.test_transect.test_plot.png | Bin 31201 -> 0 bytes ...est_transect.test_plot_no_intersection.png | Bin 24381 -> 0 bytes .../tests.transect.test_plot.test_plot_1d.png | Bin 0 -> 22478 bytes .../tests.transect.test_plot.test_plot_2d.png | Bin 0 -> 28757 bytes ...test_plot.test_plot_no_intersection_1d.png | Bin 0 -> 19189 bytes ...test_plot.test_plot_no_intersection_2d.png | Bin 0 -> 26461 bytes tests/transect/__init__.py | 0 tests/transect/test_plot.py | 95 ++++++++++++++++- tests/transect/test_transect.py | 98 ++++++++++++++++++ 11 files changed, 210 insertions(+), 11 deletions(-) delete mode 100644 tests/baseline_images/tests.test_transect.test_plot.png delete mode 100644 tests/baseline_images/tests.test_transect.test_plot_no_intersection.png create mode 100644 tests/baseline_images/tests.transect.test_plot.test_plot_1d.png create mode 100644 tests/baseline_images/tests.transect.test_plot.test_plot_2d.png create mode 100644 tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_1d.png create mode 100644 tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_2d.png create mode 100644 tests/transect/__init__.py create mode 100644 tests/transect/test_transect.py diff --git a/src/emsarray/transect/artists.py b/src/emsarray/transect/artists.py index 13bc62e..1721278 100644 --- a/src/emsarray/transect/artists.py +++ b/src/emsarray/transect/artists.py @@ -55,11 +55,20 @@ def from_transect( depth_coordinate = transect.convention.get_depth_coordinate_for_data_array(data_array) depth_bounds = transect.dataset[depth_coordinate.attrs['bounds']].values - holes = transect.holes - xs = numpy.concat([distance_bounds[:, 0], distance_bounds[-1:, 1]]) - xs = numpy.insert(xs, holes, distance_bounds[holes - 1, 1]) - ys = numpy.concat([depth_bounds[:, 0], depth_bounds[-1:, 1]]) - coordinates = numpy.stack(numpy.meshgrid(xs, ys), axis=-1) + if len(transect.segments) > 0: + holes = transect.holes + xs = numpy.concat([distance_bounds[:, 0], distance_bounds[-1:, 1]]) + xs = numpy.insert(xs, holes, distance_bounds[holes - 1, 1]) + ys = numpy.concat([depth_bounds[:, 0], depth_bounds[-1:, 1]]) + coordinates = numpy.stack(numpy.meshgrid(xs, ys), axis=-1) + else: + # A transect that doesn't intersect the dataset geometry at all is tricky. + # A QuadMesh needs at least one polygon else it throws an error when drawn. + # This is an empty polygon that spans the entire transect length, but with zero height. + max_length = transect.points[-1].distance_metres + coordinates = numpy.array([ + [[0, 0], [max_length, 0], [max_length, 0], [0, 0]], + ]) # There are issues with passing both transect and data array to the constructor # where the `set_data_array()` is called before `set_transect()`. @@ -112,7 +121,8 @@ def from_transect( return cls(values, edges, transect=transect, **kwargs) def set_data_array(self, data_array: xarray.DataArray) -> None: - self.set_data(self.prepare_data_array(self._transect, data_array)) + if len(self._transect.segments) > 0: + self.set_data(self.prepare_data_array(self._transect, data_array)) @staticmethod def prepare_data_array(transect: "base.Transect", data_array: xarray.DataArray) -> numpy.ndarray: diff --git a/src/emsarray/transect/utils.py b/src/emsarray/transect/utils.py index d0e84e7..8c64a4f 100644 --- a/src/emsarray/transect/utils.py +++ b/src/emsarray/transect/utils.py @@ -92,8 +92,10 @@ def plot( transect.make_ocean_floor_artist(axes, bathymetry) else: - ylim = (numpy.nanmin(transect_data), numpy.nanmax(transect_data)) - axes.set_ylim(ylim) + if transect_data.size > 0: + ylim = (numpy.nanmin(transect_data), numpy.nanmax(transect_data)) + axes.set_ylim(ylim) + axes.set_ylabel(data_array.attrs.get('units', '')) if landmarks is not None: top_axis = axes.secondary_xaxis('top') diff --git a/tests/baseline_images/tests.test_transect.test_plot.png b/tests/baseline_images/tests.test_transect.test_plot.png deleted file mode 100644 index d5233f6f386abf823cbe967d7d6c0ac25a9328bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31201 zcma&O2{={n_ddLh88gph2!&%NDdUOEg=C(GCWInGGLxB1A+w0gWXde_7zs@h3Q^`l z;$2(y`+k4_|9idHyRYl>iF1y9_I~#BtaY#ZzSk3`eL;nsgqZ|E5OOtDC0zu;FGmm@ z3=tu`@~TN)8~!17RyDefAQV>Ue>es5Pp%_~XS|w{qMk?Ew-4@yv=hvPtL{lhadUKO zv>h1fSeSDSkK>FP+LSStdXe8)7OXhW!)y{+@z{vENt8J%EbJtGaff+sPKA_`jiQL| zouS`>8{}S?c3}L(1>d^O4*^lpI`t5B|q`1nCIX-{XNC#`53K zI@w*g^#6XLyYYX&@mVd7J21blt}ft0bt}fakx@Xu_~e2_C>-J3VeXvVTwFPg-akLM z8^;)w7za;I8hZcwR%^WR(Q8T2!^_&T3w(ZHqUw3iu_ znH$IW9{hIi=<52CY@tWQxw`yD{PgM5A{21tIaoh4qOj1=9&THst^fj3?Gj_j$#?Hw zyn020cjRcti~AJS;{pbytW!R}96LHYvu}QQ?(FQWU}>3^|9LcooQ0B+k?8K-yZ8hI zpSIU#jfZ1GLlwfPd7O9F?Sg`X+j@J6<>cg&PdPH1I~d+{xL{Iy^Yz>_hc{wK>-y*Z zwJ(pH+F!iDyL0DGfpIl|62>TQcE@*b?Z};#sglJDZ;Fk?k=6vhOB`yt(X-Xoog}Jp zK}7~-%n9AODlsAslOo%@yX`|mN9hFgJBp2}IQlECPN0L<6Hq~b4tPpOt3O@^{N3i# z1jV`gM>`V*49c&KaIwn!u>ShFaz836>dFWCgFPBqugzBY=tz-)@IZ?A5y!XB+D7v< zM6KHK8-GsSMf?^PuDLFb5)cy;YZvbSZg^x|a^Az^^pwXe`9Ov&i>Rn*vW%ygn7Fto z{N=+wK2A=Y4-M|!=owpleev<}5d`in;Ku8x*^cjO%4=%6gj@Ck;LH@2nB)BXBuKes z8!j1>7(S1BLhBKBZv+wkkSJCwlOcC6JYVb4%y@|j<7sD(y}do>=~kRbcKKuO-`}^v z(=xwdq67#1IZC)&CO&?Cbs1l#3ilOr9ED=@CS&`i4{@Fgx%Q>E5@TF@Q_#WDv9sy!PxCWR(6jv7 z-CTUdy3yX=uC%lMx%$iHB7H3h$F~yQ&*M*Aq^~!-C1}xXiclJqn)Zsv#K`Tco|cvl zBV`b}Hpnh@Gp)R`l6s=dVwfru9#6qZD+Vmgy}nwvrSZ1cuj!ncw&=fqY$;0#2nf*5 zQ6gS`TdiUL`|0JP9K}Gw)b6dNi6cnk&X-548yo$i2QYD@Yot9^EeBh8)Y_-p0v}{# zM1Ol%+oE@2e}A8dl(fGheZD)>Yj0Z=P8x#_ApD8P-gZS>caL`c=Zc#j&Pd#R&v^6Z zP2*qG)YQ1u#!<3&e5h8=hx?Nx9R#q~XykK^9&-KPrv9rfKxtMuF@Gg8i zugaxf!I%gJ;hC|*OU~}@!&}nA-$~)Y>FB&Tj1Qk5Vft5BS6jcnF1l^DZ~dvWuaAFX z%XN@Jm~5z2=|cU7#TA&9*G6&(p6V3_cy4@Yo0wpzsj1oiR{K%Jci%gZPe)|5L+ObkTbkX=mfsIDL$so}M-(70KuA7Zb8* zF1}n>@B9hZulVdv3jY%epB;kh*ROBy%xA&?HBJ7ed}&s2>-KHDST}~YI3A4~Uo0B# z3{K1N>lOGVB+v#01u^ciq9@M@vOzn+4W0gtfwhQMn+D%%;00s zowKsB34~#{{_xywd-K)Bk-|$S;vI*lrjEhFU3gb3{B@!%M$n{YdcwlDGl-Z*!VFLA z!KG{0PQn5h$=6~*PsYc`x4qsk{OQvt|FiDjua&US(gyKpBw50uC@`$xl97>Fg;>TV zAwlco;}aSg*|9K^Yh`B_?9{wN`2G9$I-lRq6@Q)e+Pt>6w=wJi!5aq1KP!vlaen>` ztSiG7AK6m#rf@O4zQ*@$5bn7uk;63cKn-K|oK{g$(XhsWiB~fjvA4HR5HL8}VT9cELma|<^9xF{hMO$kjN8-RBXK9->iHeG*Tiyj zsNpW@VnaiT(93`!BuFQLSEy2myU|&Ac;sNC2r&PC;U7Dkp8wuBoJQy$P(cQzH$)>H zdgv8Y_@Phw@Carm{XgH(Q(~3#W(+(+alZP->*={Ue0h0!G{j3SoE`wt&J&CLaU z8GJN5Sz*KbaXg$(01{d0C2MOg1X440{><>jH}6inE>KS0U6F+h+Sb)Y7{|?&u+e<) zmqZ1`E~nMzW2c>wC}zok+}z`hjg2C%3)+t#Kb|ub78E46>5dMLiJ@R+Wz}x+xd+*u zxseSWIvyhiS6sLhTjr`OgYEmje~rKejuq(Og@uKoi|*@KVcC2!cSo&t;%O3R66FV)Ew-xgI3jaRAepV|qmu3kOi^<%j(Me5p1 zj!)Cm957SzQy5rSAe-9tCtd%-CVfju#&casVCwg;pTP|c4Mazej;FTRPn5Fm6r0xT zB9stm^AvY1n!GOeRAPcC+k1P}Hov`-n%EjIHG{K2lDz(fhleK&h9Mj#>myf_{)fU0 zNXMra_sPQrf++7(u+t+QXxuc*NKQTpsDVF-k_*59<1OhL+g@s&=dRb+_J6IAA~RD> zcL_g_R@!NN`u0umn7mJM%FoXaEl3b+nCz*ZM<|ZbA{{3(y>n82{`}}00r5AKker$E znc(5PsvzZrrAReWZgoy;sn8*j|1uUUSVzJr~ulH78 zP^5Z7@H(lJ6Qth=xhvOYw##@8$ug?CBE7!&G8+$>Gc z1}vTt2=xwcpAkb2;(a&1ks;#=iP~}O_O02)#p~J;&m1U@$+X^Q_X)lJD*w9iy{3i5 zS5Hp+?lF>GBU98>IsE70s;bLj3Nr+Xaacf;6Z3i zOm`HEROIA2^d4W?;6%V9CZfSk!$Mh;O-)Uw7ZyTm9N*2%bVl%g z3=yI^aUtm8Lk4t0-%bZu`l))t{6&s(gv7>$Ou49mfdSU}x8vjVFt(#-CSbLxeC|uE zCNWbO7bU2C&UF}6x#`PcDu$QC8lHvgTXXI zsmp+@!t}vImY0`r6+1aOl^LC=MIjU<#Up#af1`1>>79Ai74qi&UHeET@vbbz*HPI5 zE%O^vi9%-2voszGrFH|NQ!zATOqIAEz$AXfa&a_|_nrRDkfUtjNrEPjY4ujT22&*i zDY5qE=JUHdv$jJv>Jard#SU`_e*iTN4H{zVoDf8Fs+Uz#7T%I;1;!$em3pXIW#y=DUy4|U8j zS*)fenYXvMM1N&Nke$2m-Kq4m?%IImSOr~1U%eWsuu(}n<3{r6lp}WQ>ua~0zW_mA zdsid))SxUHFz}1F#=o5G;GMwQ+S)6r;#Z?be$9XTRwC|;r2dxdtZ`l2+UkU~r%5j? zg$C96p>!*I`v@qQW&lwF_@Y9lyP{Y;?=1B%nYL^{7nw^woJ+JdXg21@X2{+RIpz3v zx;>b5viaWGr}`yNtM?KO%>5f0&PKCJ$IaH?KgxzJF|K|F6+)!ky*aOk zLfYwU$uj*CV>Cj?#mE0SY1S2K6fG!PAFkq1?V{J{%9xp*t+@V*R{{d`#MYGOJR5{v zYN?y={mROO5%ebN0QH7Yu$NYUkL5g9e&?Gp#-xUf?d+Yg`fnW_N&L@ z&bGC+3D>(HKn|rDFSCdtXE}9^Fto+@Kn_;neHguK#<3~ZCO^M_=&uk)vr_ai*>K3D zU(Pnmu}Iw<5MBAT($WpJR|Vjv#@(+)sXG@lf~!O4Vtl=nVC^hQwOXx|2m3> z3BYs6^gE{(Wp#CuGw$DuJ$;^BNEf*}#Ol4)$-pRigVwLO^gaxq3ItEdG8%Y}IRWix zF`+{xdTUMj(ZZ8`>Z#N$7|58;`YlEO4(Aug1 zh4RI!eW)xC)A6;JNOW|x^o-lWOB~3gh~Mg}vuU$8lTBA-8(@@jr;l=jUKtq~Xq~Y> zaPZK=rwxE44#LF5blUAJZT`i~qX;^IZ;Tb-a1>3hcdN<>xJR7x4%RZk(1NG;N;l$#^qcc&fpRi)v-^9LHQcx{<)8C@cHhz{o7}SiaSg1 zrXVv{kKbFJ_J_iNHc#UrJ^FW$wPsd7y&zouys>#&mLWYcV_ib2F0?elzPA-4@hC++b@Nnwh z-d^ePUog!&3v_ZJ&pa%zs-l6YK?Bu}*9Ip{5itg}P=@dInh-4y553S;p`gD%|9_!t zBL|$NjbV9}`dgnZot$EJx0YsKW}hQSN2AE$!Pm+~Y6!rlGnDGMi08U3>)AVGlJ?_D z5XR%;;z-U&!j~T&Vxkck%Rxvf0EtwftkAB$aTzUiU|_bPj&hZQA_5l@KsSKVPm7Bv z^09vV?0V0@09sh-Jk{ev*N$PccRkb3Ab3EB$GWxIOJYU=Sy?u~ke!gEB{%f==>UsR zLJIs@4|Q8dQCV|1l=_wj`@hj5%^&Z`%g1UotTG;y=v4z>vo>zx^y#&M#h~PQV`Jvg z(NT#QJEJZLs@i~|RO4(@)6wD#Jp{2tfa>sp-O?_^RLVwoRb3s9g|#s61r>QYq~LSs zgEKqn$^phxR2!3Dym+x>2i`^z%G7)k|78;3MuODyuux508P13T#BJRkM2z^Y&34bu z#>jWURC-xrQfq3Xq@sc@k^DDIf4-PYtPk*t>!~vp_>j#4ecw>4YU<9{mja>yaNN#C zk16*{WPkni@~fw#US3`~Kn$jKL+UbK5xS0@zQY@PeWtGPhDn;&mk5c;REeAUi0;@A zO5SAx6YQ20v$Q$Foh%`Q$Rw7ya1OF8^2!}utPE6P$ca zZ60u|B9TjT?NjlIAN`F^WP`n9h4V=TdFjKxM2NOp{@_9I1{Xraa(Vm?EeAKpt2XtD zZG=NZAwSS|@|5j^l_g#yTroDDR`s2UL`uKQ`JTfQ9-GehRu+qgq^Gl?plQHeD0|fFlO0`o zANoZh)D=(n+$YY>+bX%hFR$m}1B|7hz~=D_ZD}Pz-&eA{sc_?ZeVO*p(QETjnFLgN zlH#cKvhUi`V{wC`0vVQSYuY8q!cv(bJk)zsA>)69n zsn#h};eD&J7kDap&BhuvN331a&pSFgQUMbKsTyT1BjH~Nn}h_(On(E?11sB5nG?GY zJF8o9{b1H-Iely{FsT(nF*?Su9044-05yT)lOzERk6ymZz0Z;ouB&BrSj>tCt5(NDCc03IvmgA9YF+UxCMq!B+PodS<5q)m8)(h(Ao{PA+ zOQ;3VjcGrsyOiPQSN%BTZHJAz8fCeT9oM;2j?L7F8qc|&{G0j|J^VO{)Har3#iBHt zJ`Z^8Bo7R@g>Kaoj_elqt2k(!p+`8>D0vSa_vG{};ch>97pXg|$LB5sFUIM5UUlo9 zuC5OL^`xkf+U$kW58uSEPve%36kR|0^SIp7@0)wC9yc~D^SfZoWx7b3^ed(Exb4hi zq=zH4SKauy6?(8ErS)9D-gd-lC4c>8`sv>2OE=kk%1e#;5?6jnVjA-C^mdxV-A&5v?mmC_X@qa)keSG|Fy^9k6)a8bFrV z8)0C?9Z&J>reLpgje56@)z$Uk3!L1xtzB~y8dz0cVRO3&9OvwIy*W8A8S*)QIEmA~ zxufnU)>$+^^Ju>_-}(LfId_xrFBrbfK-Kzy@EmD@AID8kZrs(5ONdBsBL0ju*)%*$ zuN+V*&zcxnGN+-``%y-C+)E z2j8Rmg9YoHC??wcWFNaeO6@8?JCO)MORB-AJGs3!HP64;tSA3Xy293^nAvhaC0K2yy{ zYL}=wC&cNvHR@h+1IuGBa>Vo=3H=E&-eM^TfJ4I*5-3z#kb7uQyQld{lHC(qvmg2 zh;W$jZRT+%G&EN}t5fgN7rSaCwk5EW+2%+$U{o=w-jor1rN_GSq?b`oOKNINT=!e9 z;apwcaBMTlc<_uW|2;WDk!FuI|Ke4-lWZS*9Ourm;3zmY8`K(oRLP%A&YmC&p22J1 z8{=9D+wY_|yrP|U#Krf=!1|O+{xQ~LJGGw zPxE8tOz3~uPH`KY{?i!C6xGGPzGrVmpJY`^t(&7X6MC#`(No*Xe!$-n6o`g`7HMS5 z%hTm^Z8(}I$9p*r8@MY#Z;(mNetn5tQ3_p`eZ<-{Fh>MJd%ux8} z-@_kbovHr7G zo5`s5wp4dIk==u8S#jD$ZXDGVUXS7Z_^QYAcf6>{4!pJPXiG@)dU6SpED5wpNe!>) zCPx^c%!2Y#V3vrO+Zss^e|hqAk3?lLPx!gdAZ&(?Ly;)4w=A3oiNo;Ssdl_h-i@hH zOwExtncS)BnT*3Hvhwtd_f*rav9$E>6v%RJt|8?&xRMkG_2L_xFd&iV)Sk>grBEyEf8ZV7SwUo0MYcX(6GOmhjGpH*6z^nkSNl zH9GuMo;f+c+O*!I#qbWHhCQ43ZrV7PdqYv-C5E;+xUFTB{Hl8jJe916A=@7liqVVR zdVd{ogYW|I1+AT(%Hr~6;|5THbb*pkadr6EAJzb^W}uYRuC!Gv+4=~a2C8VncoKd# z1HJ_`b3)(!ofD*nwmvu96t5}>_miY(KiV$Aa9EO8Pqh#>Mm}Ttsl~kF8OME&(eshf zIU)fZwUXAd+c-o_v9_;55^WXKkLdA6lXALt(NV{i8D~<)js|0;1t<+@OhWp`7=I~<%#k~ytwNeEH3SOs0) zoV`io+^>EUUmTV#~Xdmwsb1iRpxx z01_{J4|upXd_N~5B0?WWgMkx2gp4TwDkm->p`)M^8}|e4d;ef>Ufy^E7Y|Pq^wVe8 zUv*74I!!f(!Bp&@#+lZF;7)p}ecns)i9+x|PF8hz$G2w(ExZTEl3s~vv94LD)0S~_ z@?C1gj!Q;U2h*u391yhk$GLS6x}TGjtsu2C<+17KtTLBw7`RqDPT6+3}Q~i4sgyW=L0hI+R)jR*8YCdheBrU$)Yye71qjN+h9tx z@LBs0B>>^=smuu*j8P>%Eamf{Doq3Jm@-}}SvqnKiw*d4Gz98Pl<|36;~3GGC~#a% zj2dYF$p&|)3^OLJC_$90K0F|@;2T|R(7)Gz>v+(b;MlFZLmL~QV3oTq>F3V!`~O#V z)L@5*M`GR&8rT<8SqhKi9fd1)OC9iNup zRBeB6SkW%h7etv}5WK0-2>1BOllk2=pf0g-+^7};f|V$$)|Hvdn$|hvfNXOfLBdhXUOvHk5?il*Uo@SMo) zt)4?!mhtg%G_H0Y%h=?HKx~%G z&>@4f@I%uu8&HX0?Ld%~mIJxx?khmP-QhTWnh~L7VhRH-Y{A>-A0HAQA8&Dm;oo&K zUc%E}s(+RB*1OOUzv@{lv(!yi0PAPd<$b4E3$s9XPW8RY(3G+AbOCsr$B((whhw6n zLyC%uuB>luT^2I>n=AO^CWBv|zH;4P5@<`f;03cMAh!~BZ@?o0{`A?G4^4Lssx;YI zSXfZoN1s~;rvFSPa2OtujH1{LFq@<(|I?Ah!i@{E#xFe{tt~z+rh(J{;R73p-n?8o0Px8&3TQ-owAQLLrDk^m~j60=qxd_~o_=n;28tuM_ z4qeWF`JpcVG=GAPrCEy)8!A8neVDCZ4`qoUJk zS69##SncG*d+MrSH}ilg&z0$@uN~HICM{2sN#FM>w$FR*skTA^x9TmqQq|YEx?a3` zDZKln@szQ4lEs>A42UfbXMgNi{xDOV?QY$hPbGS>W9oZel$3G!Y8M&2XVoZcPQ?cK z#p=ZdiM4nIRnu|vU^V>8EA*FZLt>HMIfiOnT$0~%)VNsH*o5sZew-fdbGjTZ{=QRE zB*sN0ns5wvd{}Jb_lJ|*Y85Mdf|X=^TI7jpElzGUB5gjzOk9`7 zeM?Su$bYo=bkc}>wQkxSSCAcGJp6KQH;GOqPJ8`Q4WsIX3osY$6`v?7k?1@n&E{uw zZY)xGFTphAE~a(NpfABHJ}u+6KVLI;Oib_M?XYx4+W&(@5!h5Qxu3sbeD-oO5LHrT}YDEmz z-NV#Vrg1&$Co(BRep8bS%Tv~ z6$M}LUe2~B;3;^mEqiHt_ncZK+2i>KRXzMsEL@i^y!f#cu?Auj*fHkhTX5sdR2ZL^ zu`R1FG@Gkx^tQI2e}wVz)ymWU55D5>6APl#^Xh8tn`SgL{*U)UOg!4(j`hFsAQ7Ey zc0+&d%c|njhuX^F@&^P3bL$q$R-f2PU@DZN-Y@1v}L z=k+p8JB_WFtUfYu33KXC48OUIz4qU?6e(C9`NwvJz7J5HgpOObq+|Np7UPq&#lM7@Z4UBD%WL!-0_-z#?{fL6pu*(Xhr-CdZoXh#w z&pjO-i`~C)|8mInS*}au+)uE&^Ns7Y{B)1^Ok?}Rl)MPJjfPZW(}H5F)|0e+t1&@j zkFO>-_HYFO6nZcdMTzUD{k=Po8>ju*ymnJpb|lDo(qrhiCC8Q?)iL;6jU^PcQUy)aSbTD z9_-3RBOGKre@W;#4yC^STXDMGb1~Ae>bMofJ`3&LHm|hcH|o~3d+RuN{u4P73!a5a zr60m!)W@zB6#SWV@3teyro}tPD*2>mD}WA|hoN~h z5^g%un=pnQJxiFBN`MhOLuxotYIY91ouw8n(a)YqG;WQXpf>`VWNcDW2q5lM@r!>` zRt{Gn%%R+>v7g|r$OU7p$Mz~GPwRqJw`m*fUwd z)dMnt@kW25zeZ{t@qbUn#osrE zP3&egB#BDAmhE~h>g7vqpfVLU4CA=L8f3XX*L&^t(?sc8sHTq|{BfEef z&=bL{_OkijU2bmfx#y*nxL|w>;#S*Ue3H4@MTfN)}~8f96%2w8Yj^~dd#nUWF) zuU-2l^Y_6~FotGoMAwO`(bs-)8tD`1Qs(E|{Lu1z)8=cxVrGCDh4 zKT5BB-|>yDEf7nKu7pQN4~f@c`ceNO82#uq2#21y4JOvi`^YJ%Gx$HvdQP;(E~=%b|FY zq1`v>L!AqrkdRR7PRO(7@bK_wN>-)A0`;%XGGvD8mJfm zOABbwLf|V20nUSoRa}L%7;t6|_}GBzfICa=hDD>)aOWE(m@wdDUz{#()_HC5gDnFl z1I3XeNAwD`ap2LP_wsr^?gdIX7W^t!PEHgS78YQ{HmOMa3rj;a`GEJkM}t5vyr$(z zS58QAEE4oa8(7^QTdxfyH^4w0h6XB-Z^4fpQthVyDBpd`^9U4C{gV23q+Wpk!zrGP*YYkzS`32I{IuO2Sb@kW^JwHSYHf)-`I4 zdkN0(+*8((8}q~GlI&1J0qRnRkwV`9+o$zVnlw_m>l+dh!o|&v57*KO)uxRA<=LQ(-N8VmT-4T#nDo%uKJWlvR7buXEWR_TA5w6c!Po zgfCP93(2jhMHn;SWR@o@@IZ9HjlCQXrfCkSfgX!L3<|<;T5Jt1IN_Qvs~;*Nux1hrhF;ym1Jfqn@y zxH0^}kGzPn?1UZx_$f3jtgX&v)_H%=6Kod<*bQY-Cj>|^sGaK2djl$K8+ddvzcl`? z+0NmOl44gusq=bzwByA_fuOdxy?J9Kem)W001#KFKYvcNFX96?XNk1|n9nHL*rJZI zol%J6CSy58_Ug$+74QwhfS^&My}SDeB%g^CjT<*^M7Pan{sgNJ>dGqk{Ysi}9Jrn+ zlE;t1TV)@W3cke{Uai#D@o{}|VTd-UYZ5r6PhY;|i;qKuLpugepE;8Qk7!{uj}-K| z@+*VP5Norbj^19b8Wy>;Y^eMbB3?r6)+dAT&ZE^*jV*HM!yAdvG;1}kv?WI77LL{d z*#@`D$a;d4Q}OnZa|r6R(!F#^)Up+k0V^}uJW+!XNcnDE1$u?lM_Et%f%kCkx%bEB zd+3CWi6K(%^V*-NNy9c1!$AXC;2T4EU)((C}SNd|0x*@^kdlr}FK=~CCre#Bi&6h3i#5Ai!`cO$jiqrZLQ`#RY+Z!3v5E!tQS1!8 zQq)GTRGQFbxW@St7Hz;l)o-7@)2Q~AAY{h;P}t%;SU95I_P|8pP>Y*d+}qiZ&TwBE z7Yw?8Klk?h5HE}S(hlDI;vzA4jTfqxTul$&G%R!f}l*}?MTfPn2Sq9+i&_eD=I2R z_DtfqOS<1|*vgj0LFlvT2!&V%J`9ZY-|Ksz139={1U}maK0wr~kD8qB{#f>>#D&%Y z7%OpTXDMghN^V@h9M+a#EW$^iNoN{-EQlZ2Tp*-OwSq~IY$_D|Sm2K&H$Q--1ZhDU zx+cnDZWZXgCGTXGxaLP8w~bnGQrV)TqLdXB{HDGK=34@FIxi>?7D}SKY-QIaLMD6` zGqEFT@P)s!sr}}=sukL>UrBBT$}_q`<~b?%vO~3$1LK6Llr7%%1@nrX)hHr2G;U5f zf%BWaC~5xzsMdj^HeHM#E^9M@x(!iA#$y!$&N?_Vlide;jmCeUdid7?qD4vcmB7Zmg@`XK*s0aFC=zZ2r(DkNO%{k@&=&&o=A?}-KIxU27wzCOX! zRo%lQ7NXtn$E~Q#=iVtKCeEqW*TJy3=!dP~NgUGQFi)jyw zC4>s9DO}0ts^CGm@VdI`aj_*KkLP)K+n&jfbI*y@L^$mJjQ(++u2jS@<#aQpG{tj_ zKm&UvLc3>rB(~ZjWch`BgOjKmr$+z7b-e@=OFqS`I7@?4A3KKS4%>Dh0{HfV8l?(zN?=2 zFjM|KuJ(SF24GvuF*(Dx_8p_^c``lJLB;dMUrqGM)UYbI`Hhk|`^Ab&s&1H~(H9LS z>WTbT)2;sH)zy;rIl-5KNCLz`dWM{ft26jvzjV*c?6Zc@ma2TDlJ9xKSM13kzs@Lp z{=)s8%mDe0l49Bja{DH!yF&~$t)uyqo(bHl=c9+MG#w`STxA8-{7SH%cz%N#jWxlD z*z3NI@oF`QRG*r1TJNev1az|FoE*i~@cLfFIbn0953Ol0tyYKmKDT|tSVwV#Y5bx% z?O^v~D6)q!CoMRZ-X52YNsHS4-kb2M{C5|%40u%laO`N7PTNjU?LG8G5_6k|w&tD; zJLi2Qi}+}9X=H~yTet98SH{=z5*CRCfce6;F61nhX5|Y_}&K|wZkG+6{jM_ zo-Tro%T4!Bjmf0aV-(-~Iq4>}OAwKfDcRa`kKK?=J>?hS!mDl`ieDBN?g^vE7RKfG@v_nY z4fc-<>6Ia=9zT{-u9!nUjf&2#Lc2&d8L{FL{BJpk8{wi(Wf=yn68GOB*2I^6M8T z+cabPdwP0cn)K=YPv!8yKL{s2xCq)U!8`cKW*=O9It_OmbFHsew=#=b2en;HlXA+? zVS)4Ik2#XDGN{pz8~;1llSJJSP?)$!foF z%XCTYlo)@KJ`PZxW~siz;1vX$6>80V|NcF6`m)O@fp73-or?|4G9iq*&Xl$I1V8=R zv8VT~E6^vu*;n+)l4=1XBSxU>_M~)jgB6tEkjxd2gzX~cY_&pqsi)U z0O6Lc1sX`Rn+u6hyENKSqM0m$12q^D`+9Z-mfHEwit%a=X=x_Z;VXIodD&xqF6_bL z9}A?8OYOab`{7QrgIC1qJ`z6QqrKlinE{0rCnqP!F`ZyzUdue#I|4Yh9qJqB-@9&5 zZT3wY0v16%wqW9gF=cVy_@agO<;d@EABWlp^@YbT6aAZ76?$afv8Sy&h+u(DLq`Ej z!HEH<*H7twEWfeQ!$9?hPF=96qa+MG{vP8uZ<(RdVS6tAqPdvcpBv>bu3<|Dfn)C7 zP#rO084I(7QJZGyW_+v)DW=LUCk?_8+T$brrDeTOKvh#yf?vOs00Vu1knAi0uAnD# zUL55?6rleGsOL>;J{t4_jTo&8eRpfd8pBk}aaAuAhni2ciBTVE~g^9 za$w@4M)#??`^kkHKB5_;D{Q2Z2i{6Xwy82cR#&fQ<|icyFP`Z2yqI(oCKhOF{vhM; z@Beaw&RAMNl&F6j`Y){xzYj&>x#y$WRoL$ZWo2ctmXv|P$z15}zo|DP1YRiREP^B` zcdTt}Dtr%o(x5*I`sNBQJvj;tK_ny&KQML*AVz~t83+CGl(=!+iY59jN?*}CfFn4N zkFaE3VRdsf+yh78_;EbwTR@v2ff1l$WlbydfcOgab)SLd-!J9rOXps%Bh7w5Pz*iJ z06}r)hcoX3`va!pQ0UU(E9WmVn2&QrfKqa15R#s5& zjXa8piXsQ=vgSCVeif$()->;%)_&Cgjh2TX{nXXhqv0M>U_eYv%$0P|(Kw;|8Fa7^ zfV8MQ3z(ADa0qV1*0UzBLU4r8fKOb6^90zT+dH7-IW3?g=%QFSFWIzSukRE zLU@&Yfh8ay$c9t}UBWLwFBbtZAEcNWppqZ}jWV5;kzoa={De#mJd~49P>_uJ;A=nn zMCj(JGgZqAj~YRmfJO5Oh>tGl6GRXg>=z9y&7=kf6W)#z!hrv~p+OYX&nO`6tf1R$ z0r~&{S|JDs3FpCN0Z*yibx{`*EHlb=s3&2mhV5GgpR%<=O?;8VD9{7h)W=b8mldBh5~a?E?KYOOfZY(+8gF$itxj;lqf8FaHBA*Ld-3TLj+B%X z+S;8gckhh)L!tDmN1<$&I^$#p_XRB((P-Ngq=TSpxA7a$>xgfz5<6 z%@TOcYm?=i5aJPJZEfw^=wo8&+?8Ak4i2^g9sy-#A!oRDJrc7cfv!Y+s2|aGP_yT* z{s0_MrUAMhT_OBt-uuDS70vc&Y2(%3YB((|%^^n??R0`8$NG2-z2O^zVuqzdUhmGQ zcsnMD*XNv_MW98ewAx{9rt@!2Pen=T2ir81?Dj=~%?6F&+rNIzLI)hQGT=g^LfH1U zN7IL+IF{?SuRhEvqT{>)u7sTLIkTjgmm% zM;+x`RqSYmA9WVy4eE!v^X)C_YX|>N1pqa)nF39IK$LABwPa^wBZvNi!32ZExWhb5 z(lstFUd;=jPx+K3?)1`9(dGM)8;8VQfq;QREfCd$yDI_v&VZR7U3mNkrICQ#reV3A zu>t*@9K>&BOG^&axCCos7G_y!M1=UAm<{Etq!8olmlzqv4iUN7`@h`4>#Lp6B7$RXTJQlW1mAQxos)IND~0&Nrm{;9+hu>0uF z-v3o#>l}TjYrsc;c0d{=A2fCx3yJo3gGM5h{QENhdiTcablYU3r#K|YKm&_=lHKde zSVPFctI&^vq5v@3C_z3a?`r_b8SVK)*BSVFuq)4DkAV7d7jCtHrY2c{R4{HtKxGII zfBTBDbo=&}4)SWU_|>BjEr}3l3za_~bIft|f*p(WEfhd+mTq`c5hG}?3lx56kF>|P z>IoR>2(Df*yoZ z9^FXmAMyOW?7Zh2g1lwCx1gbWueKdYRs4X%aq1L34i1i2cMG~a_(^KyjIveVYd?_- z@5syH`4X_7LH9i9SB!)UFKkgU)Dc*x^1oavlIJk>&!4xqwc$V(?t-EkdU8nI-Q7{> z1MQ@!RpKQiJFPV6$K?S1g6)i}0<=NpUq}x}nOd;V*Fnn}YI%UpYLE#q!)XxN?gNWD z+j8)`Y;Q+zF{X8^pGggYvP5?QpaS*~7(E;Wd|X-EA9ju&lL-aX zlMA1OUiTNE^q}4EDB%px2|-|{;z7PrhN2EaL7iEUwTD) zCfJ*}M7$6+@zTWMb1Z&G@_qe&nxftrqS)PcIt0n7%$JV!en z(Ms{fi*r>z|I1c`RuGVv+JMx|&d%Om$c-WUnqC77(DZ)74cHFieDuKXi}OYLB_cQ9 z*U8ub4QqGX)phLI4P9_YHbnuKgvvtD2#r>85RcZNY466-Xuuf|UPEA3K=XMvv?rjv z9U&QGmWR)uSw~>?uHrgDIxba8T$qU^arObanG3qxL}d8v^t2yTp3Dz>ZUe0ljBXYI zqc@Ry0KpoSIuO6HF}=Vpo3lJ5EqAwpmG{o{6f!@Lf)t+-V1MHGO`Zv*ln1W z0m%-gp|UoYN0RnQv$p4zl;#*8*~9ut7PAX)`y(pBU`N_5aC}d4nCS>(l(k1g`h`xveqK@W+B9Pxu~6kx*xliM#^mrH{@L7Fp$Dg@1W&m7hWOhiHzP zq0pXBU=I=GZ$~s4b9Khy#D@R5o+Pxfgj8}AeUQ-H_3XO@+NWhblB0aS6JXN~*gFDt zQbC&r)8u`RAul1}p*!cSuC00YjvW@_s@H*Rqz6!-lyHAWgm4^|X$7PL)(u%18Rvuj zd%4yyZE;!!${O3--u^4yHA@(uwelNPas#@?^Y!&D*#XMF8$M$Vh&r%N7~~3Yfc1;| z0<#OXVV(}lzlWLQYZv362lNTN^8)Ho)Dou_$I&nLW94HES{z+upY7@%Gin9l4BdX< z*1%8jw?eyF)Nty*yRM`SY@mchNOSqAoE#q1VIyeFr`V!+5eeP(8H!J^BsE)nsA}Wr3TL|s{h2s#RZ@YB7n~G-7t3R@V&r< zqN+>D&WVc|O3>*XHT56PBbPDKKXGt)m;#!$cEYov0%*GHPR4eI7V2RX>mZhpyF3nl z{~Uq!=wIt?FoQ=8`|=P$Lj_E2K}*N<>Qqes0`$iJ z+l?bWLi)f3td2^ya>~yL#v##GA7ck|8Q?A=A|eDqO>1EE17cl_zZ6)Ok`h7Kp&$Yc zxE*0Mf3}6<=t~m(MZNg97|fm%K7v+MQdD$|g|H#jrFeXf-ra{LIc5!auJ6kN&OCkg ztPQlcL&(KLWW2X2p#(k$G)`1_QI%g}35=1jWZj|7aVsqNlnXVj^l?BqYEpN&=`EwlV;e+Zyf%W(Qp`y$6c@s(yf6O_myL? z2?5N!#KO3L510#&7>}29BKzDCRN-Jb<6Jy^B?ZP48eqQXJa|F_Ps@b-E} z`sZCu$uoTJ$_KF{_?WC02cU7p($O&r^cnB<-V^6L|H}sRKD>?55E>&58$CqjcIIeI zpSyxVC>u)vppc18K!u?lK&>T2sxz7BI!X|>m|7tJTW(P4BrQV}(oznNkEa1ZN` zHIgvswo}mcc?2ZP)U)VbFX)lOr4^@~LGxQ+0#_eSu?j^3M6LZ+7Na*TEZ&wMZ|zTeZ;PPf3;4LLfD0lbCKkPV^YGjClsZgPOi=ELP6(Vkv`y}u zuE3x`Pi{EO%Utm6B}>}Vdu=XQmA*fm?<9HyzwOXm1=csu>PqOSag-(jPsC>ri2Llh z&?$acI{;mnD)=8;hH6|D@4fy-3+b*_YObHu7PU7&ph`B>Z^mnnEA#U5vf&Rworc{* zUK-!|I>v|EtbhFYu?nsBKzR-(G5jm+nu+juM|(zaQNHRV1dLLF3D`;ovNn3nPd%&= z*ocV{innRd$BZ|?M2XHt*gFMndquZffig1!ZF2=u@k;L@q9CgkbW60Zc#(FTHbP5D z=`r^m8VS>AdPC4fgR85*XIDVI6a{VgCsSz=s0axF>bw9bcMD_kpQKiTPaspqKv_q35`qSYUq8M*h>gvvMu`+1 zbmg8tjpAy!x>sX1uK9fUpg7{gJ{j;j2M{C-!sZe}(ljC(4DArCVC7&}&b(263{#g2 zyue^7G46+~7XgKtHH1#o#EkZoCS8^PvA#aq!1mW7JL;7tdojuFp){n-KOMi&{ZSSA zcju(dIO|fulMA#YLEM`ZGdGu!6(GA&H|l)>dIyRr zJiv+s@NOOG5yi#Dx1!G8{ow$xgc5cN3~FhS=LNDH2#GY=yY#SDx_DA3V0`Nv8qiH1 zvfO|I427Iwe++1N7ML->ND0CgTolN)msvP++tYZqwzk(uv_UL|tn>2hej?xGn>U|N zmi~)rOy}YVxQ*Ns)jw9g@95~bCTm_{O#rSY=gkEjbS5GqAgB+nL$zgvZqH>@#fx_H z!(KLXp?pED5WZFc{-PNO@q;kZ2KolH+0dYxkzVwU&H|y!&y9^&UZ&vWJ!CgmVgDms z1YHyV?|S8x;fH7dA{ie)AwY5{B+Pz(ttFBv#p(Ue_fs!)@qfsVvZ||ZK=O@EP7XzP zPLhF=e5}lZ9f*r7f|hj872lJCog^G}l>>1FskR-kvgq|u?hV5xDXJ;OeW7@Xyr?R9lS+WchrLny+Buj}FWu~lQ8ihzI$`%ieEl9XFH4O&JaAJLIdATJ{IVeJQjR$>soAm&wZ= zOWg{)KLzlBwU(a+UuW*dwZm;cseD?uSh&sQ+tTTerl?PuJULNg;t6%#J~J}3X5T*E zLgYs;UJQI>Y^fe`r{&wXZ|@fu`$IvRsbSCXe$ace%vN{b@ja{RJOS(S%!ZB*4~MiD z+_G@l3qg<6VCU&~?TP_J3tTn_D((St0i&b=l6e8}3)N3%RA8#a;yvpHJC%zgx@{?M zO{ymm8S&rC`^IzaK%ZuN9%!`yWlMg|#jTbN-WZX;8wPL)Q%}rl^M+IH-2^ zZ2C+olpPj^45FAu#9<^UWfWWssSO31q~yz;2;C8BK(RgeQ58nVLd5`;>d2N3S zhI*Je%$vNr=ED1uhz|=6v#~V4a`lpJ%d~HgSm~6$CT)R}_A}Y2ZzEowSSq?r7`pe9 zt7&jpdy__OT!s7k{wL%1$lLx$_J4X08Cm+eRV1*Zn$C9V^)KY1Vl&3f!X{fazp@?y zc_1TOjZX~WlCCn@GM$$GH!x>g*5B{0-_iQ= zzujM!O!nqhs+v6P8*uW5VVrvXt3IW8smF~TIZpE%pKu48S83@z*Vg~Sd}RGEoTeIL zSO1UO90{r?EKL4QvD-A{r$(Qi5Ot-Yz70Wfi$ki9&Zplm>8i<_95vRh?K$NE4U<*3 z#XWF~{;W|TEOGfJ|KQM_Tq$$lx*x-F)$I@|Ug@6L>j-arElzTtIw&MOYa@2-2x4%9 zR#9x3#O+O{%0!BmYK@w95Agojz6EXmE_g%37^U{5HWSV5Q!iy^j_7A>Y&rOsUoYIY3rN=R=Z3lWYoa|chTeqmr{rZhhFd%P=%Ux`2teX>VB=oA!p4}gI z%Iiaca-YNu3#riw@5khaHq+>|Ujn7a&ABzexB7h*6b=3YIPOiBlY;7rjC`-f-tLxOMg}%!7pU)M744y}RbKZb%5nDqL z&QqnPQ>}dLXvKnIxuRdv>)cr(a2ssMe3KiSPNk$&%}q~B8(uljW=o=ufR%T$tNt<; zsb6MTr3E)sAY*nodu4ivYRG%$($rTq)YB_%_pD}GrVz)5m-p6uwmCR>XAkp>X=z-| zeBKL-@%YxPiHL}ZzV$&5AFa1>iu#Y^NYG4=(s@twpFgw_J0k8d4fN+S?!BYD4pMT% za1o$RWKv?#6}9};0sGAEAC`5r<%p2V$ZvM#HG43ZVib^D2j651qsI(7&{H@m{wab=HBnzgyz1|x(6NfhVQ*xcZcyZ4!SC`DXvB+=pxVO_{ z<9|YdojTp&v4Et_=DDhfMJ~-7D+rtE5cIqO`Lgo}XUN=YEQ5C;A>Cb0q2< zo*1X9qoXs(r>eW)msF2M>77y=`-~e&F^q zP#UX^b6mUaMorAZ|!`A_q=M#pSO%iEE^fi~qWHE=;-rg&G4u z`BOXZ0x2H*Zv3=&QuNuwIjr`wO$qNluJb&0kJk-j{f{=E}$*EmY&oEt_M%wu&h!O%}LW zz?pZo9n9@3dg5-2VaiJ^5QE{jJ1A=^V#J7;J5^PHjr9?GTci*|*%kvqa9Od_;vV|l ziR+`Ncdf9^4gGfi(9}S;DrnQTKzwu+3J8LG`>%kM%KkY^M<|q_Iz?SlK66nGsZ|w_ zP4aH1=0Wr!ZHXYR;?dRXjK8O>FP?==zW|8>z^S&b{L?LC4lvU?5Xo0iL8J>Iu^^qP@=A0Ni45f@c7 zCnmqdp3V;3leM{P`P)}m;f}S~XJztlt_Tfc7fEwMZz0@a@kxirv4)teoY5633sgBLmY9m?Q)8Xouc@XBmnyyO)}R(nYv44S(+YINJVDUnV`|?DLGG zw0llCD+CSYx+@QB40g!KkcsQr^z^TlpV<$~xdVism*75z(c&-A^abIwwcQX_y*j>l z(a?T|hBCr!(5Mj>t}-eSaCL6s>DVNlT=)CaGG13Mm3XR)o4sX2`JG2Kri;rvBASL^ z@SV(fxoxqEb~8S_c>RKO)qG9CS0T;I_-4@eW$irjW7gAHV^pW@?XfkG&?WZq?9agv zMwmseqACNJ^T=-xH+6YUaodx(X27w~o>sDTJNyu56am6aV0^Pq ze#!F^=)|a$B{iC0A>Fq~p(9G$EhvZXwjNHC8hPmBIF?4_XP35*jF(2_f;c@OCjUIh zOHZu-K-fFuqfaCJyW=Kh!u&oD--gWr8GiZpMdAITqMuya*FDpk6lQ>+ntn1QMA~H@r9tD5 z&UO$Vf$SpS{&5sq8+h#eeVnxqv@>0DL_m6L9vBH|-OC4oU&+!TPHC~4wODsr_(g6v z9PH>f1h?P`2PfE|PZsiPW}m+O)GRJErG(K zc}wD2%*ywe6I#3NR!vqmRZ0Ve*B>|g>o&-Bci!{V7t`hODX_^NFk~_Q)pN;E&2Cea z3BaY|(`yoyUOGkZ&UMvNWBLNACh8PX|Lxz$^ArmVA-E97(xA0A17{TfzPbW<{ylSs z$KxO0>7I|hT~h@(Y-`0mgz=O;c_#a0j1v_#HBxDG3+%S<+}nd@-#Xf?mt$gH)>KA% zGNz{saYkJuV?{9xU=_-mgkogPx2`}Y)vDb2a$B+_TtLr=gxRK1A;om>qT?5rZ5Glm zq&=~U@1J=Xt7=i%S3U;Nfo9pkcW41*6ACgHC@b<7+=iO{H0m%@MyW-5Pd=_th@ee| z*la}Ado_tO*6uh7zABv+)yD5JtvIbU`I`|fw7rsFqT^sz-h;E+=}tNrE?feJDJS`8~;*M%1rE?(T0V+Hb0p5ooC zXBP$(IQ<~R&GDW~c?e04dNWq!Lw&RB_tns$P6{m~XMLBL1qR;EQ3lF=OZx~2^I5iO zWbQeef*OkOz2c4w&nFyQ(IIi^-`yUK(yxpg`+>>>q!gxq7piM8=#K~%V7eHGn7u1FP*&NYUAz5Z zqOwt#DjS`k3%(0-N9^(VQDs_B)2NbenTC++19=~+3c=UmK1kco$qDmLPiOY2Lj(}+ z3$HWOg-1B*DU(q-#De+m=MxUuH>24*OF4e8sOWs9jgQE4D5xcl<9Qp_F%w2?c5Te> zf7X;17w?&W)Oy75;nIk!>Neoel<8Z3<`xzK?fOzgNDkDuFGPDFrPbDra+94!%m$+o2bIQH zZ^(%*!ns1|G;z`-JZ&p|9oqq_p_cvD^RDONQ+ty$M^_vF5SDwJ$uB7Q7ljilX{IRISPpX(r&F$%?k2Ha3};S1c%?3-) z=4S=DP(EuswW}yDD~tL4OLYZxm-x?Sv+OesK`2n#X|W5fn5WHul&33FRH-KvGv02S z$3GP=CFM{9p6AFNgIZ9C&Ll8LRxq6jVwLQqP?Hr4g0l6_&gs80PXrcj#6l4)PSOX# zrjGWyQkl?a1V%wf3lA*y#nyPoATL!2EUTlUqAZl>Ru^Fx#gTECV0 z!eBS~TICAoVE{V`_u%XccjqkyH4Wk&jTS?@1Ix4EU$%hLCd?y%e8rMeBX;Az@Rb;T zvlstvw;8n8tdDl@8%4&{dS>O*Dee$9FIHckI-A<{RaJ=GA_k0QrL%G_ zRRd*YAG9DlYdSg|pT9G8DU(f`g#-Ij6R=^!-5LjmrQ|3H4$tsu#u$4NtUc@Lyv8Gx z&$HCYGpU;^Z!o==sWYjhor!mMRaMnz4C|vI#e^#>d_ub~-jXrrNRy9fPqgLgzn#PK z7k1y`-%c{CL)-t~f1vHWMgRW3sO3E$8EIH~-CfK*L`Sc!!|XM|-Bcj(*tCS_dk-t2 zV|RTtiK5MBIOIFNp#8w+ZI6YQ!!tWju?7%Qr{>N2c_aiW(N)rWQfF^yUx&jRjAhNj zCt3q+IGtiw6qJ(jQr=eILbY-&Mp<0kaPZ)hs##l`ZVL{!d#``dcB}*S9XbPm6Qu6r zi*Lb-?!ysba!3Vpwp3V%Mx{dv5B9`?nvJj)t{qLe9ZdlZt(u|^!ayaYhCPz0nB|iS z8bjihF&%IAJY`?ff*u^TCg-p$E?m}DhR0CZ+fMb}nUn)?!?fs#j9CRyh8ubC+khV~BM=iOYvJ@shca&sek_z%-TbBd+?R4kbfGoqWni zdyBqsIOG^1;WqmG>G6h~@aXQT!A?tywc&^BAx;;wh{Wjwm@Eq3FOFk)dCL()r0MWt zOLZA6!*deomwf?i;h;k#z8kDfdE%>JvB+UfTrhS{S>c&ZU^#0zy@F;ECT zT^IP8Mt8n|r4tw9ub_{qV$c z509RBbKNT`Su3&}S`EovI2yzx-G#&>G91Z~ME46U!9ot0@R&nHY+k!_<`E)IXhBUs;K2*;?~i` z`p{7FpS&u*85|0@e`z_Dm7ISj06V$Wl1PY` zz(y)&RRvY?Dp*UuFnkuap?IC|9n}E>k`#GD0}yWVjZWwUB3_RX;M8z68`!{B`U{90 z3#Q~Jd45;HFld&Jnl!PkE!F!n)RniZ;SjQLS$SpYlkp_8iW}}xvvfjxO|qvA8c{hh z#FgVIbSW$K{IsT1qo2cHKvrpTn&;(ce`J4q^HWeX*b9d`Y{UTY0^Ia2(#>xvt(7c$ zj&Tmc|Cu50=yy02Vmz563E_~R*E9f@XHU!diZH=*6&f^N@Nx# z5Yb-LqBOp}a+k2SI>7UCSZsD>VkP$iS5ZIy#-4sQrg{`ga#*}zEw&w7C`~VEx zoFM2Bt`l3Ut>ne0Ub042j?i2b48h49Rh z3ZO)gt1=oqbhqg3^hiECjk(5;r1M=X-qli}?%F%Jgo=|MPFmO4BKq37!k0UH=Dh`> zMyt}*qgS$&Mk-nG4F7W%IdIkaypcKv)v-#z7O$h0n z9fz%-XRP~s-*){`Z7qNkPZ{sSdIy*ON3@O`U5UoYL-*k|F5?Q(wMX;m#I#`)dm34GG6;i+SP7Qm)UDVq4AmZUx%&H|BQi{i>7 z$Na@FEb=YhpnZhH7##|nyTt%28a?iyLVStJOe0sz<%(Fa`*Jy zUUd9PJPJ1;I62HtRgn=NB#i(IqtOtv?Iaz%B?Nly48RF+lgeuqohmUxj_KeWspO%@ zgtY>4Le>j^`c{~~hT3%loOp8%jY733du2X}FAEMYsp&j;kr3t{zS;p7?mlF)PLRDe zaoK4ERYp^mfO9G{?u?obV8TDY*4mXa+7Bf19y>sw8Q~jx2kxf66kRnjHl6S5#aX9y zbk9wj5H5!sHKA@iC*GP2!tSxmUJ_p`pd393a)7@gC$|m)^%cDVi1w2?#@E1;1p#Nh z3|d&y6#U4aV>o}I5ybP+Xr`IWSfC2Y;Uy?w&XH}zF$AvIqq3t-W_(c~GCGjb7f%Zd zP8@Qc`&t_t0(1m&>2{nNioV2i?D883&Q`%h{9M91$*tnf;zB@Q6{5B^XvSskfi=@u z7_6|9q^qE4DWVE)s1U~xQfr!={l>NKqj{JZKV$t~^NDnmg1v#?U1V)&&)F%=(XHGn!6hHbrB>TpV_T-v6|4XsMqnk!Zr|QP^oX(=3=BqkHtjy_`puiu;X4Ysj;Afbff2yOpWK=4514m1h_+9# zbn)R4vki#Z_U>8o?L{}tK9Hm)6lx}yCIySVy)OXgRlsYL8eg^vppdw}?5cKUP$-9z zp-dLiYt_@jPLyRPCrwn%Awi1Diy(E5h8}pSAh{giBY@6^yKG8nabjVH`%BvpK4oXAT@AQfU!Uk>Ah5^j65}X@X5k^*{*Oi12P23F64n z9FOB>1f_`%4m;T?93om^TrSpO!98p&)$Q_Zgf{LF4QuKgM}q>;D9_f7`8B53F>^A^ z8R~*MYpANJ30naus}b!Q679ANn#PQ-E0oT^AN(}k$~2)#tFFRkLa{L%ezBBvZ4MJW u16u#>G)6BiDb(wJ{xKc;^ar({Ept(sleE55YtC#|*jSIY`qgsU>i+=%G{t=Y diff --git a/tests/baseline_images/tests.test_transect.test_plot_no_intersection.png b/tests/baseline_images/tests.test_transect.test_plot_no_intersection.png deleted file mode 100644 index 70a0fc011d97ea56a4df3e951821b7e07ed9ccfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24381 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 DGvWGM diff --git a/tests/baseline_images/tests.transect.test_plot.test_plot_1d.png b/tests/baseline_images/tests.transect.test_plot.test_plot_1d.png new file mode 100644 index 0000000000000000000000000000000000000000..748b5ce140429667d217b0fbc766292eb19adb30 GIT binary patch literal 22478 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±!exK=GE#<~(}JHd*10!4hqFbA35fdBS9&_T?P zr!b?Ervdr00*{E`JfucN4Gq6k$GdBVkC-7nG$Ezdo{4}#+&d*DrGe(n&w&n|Sfey% z+!e+c=qT7=B0zPhaTPQ*nUM{PFuZW@-o4T(jPdzGSVuoHmyGgY_{Gf1$`|s{YN(kj zwm9Tryi5Gd4}2VKbV8~C8Dg$ zQ0lA7;}DDdLgR-A0{QbXvdAHGHP`7JYSf#R3qI@YS<9_BIru3FJ6UXb5|IZL6@sExf$j zPvnA2@q$=R{EYxQ+_Gm`G19mV(4stAYqdjX6$6r;a2bH(PBk^(%I$xWVV<>EZ0L<}O1M(btUr^Q&p)M_HDkE#}h!ZtlGG7v18BYeDo#dFC1+U46BRZ-jl*O$zs#3GhO9eY&sob z%o0VG{sPfB)5{hS2v*d1zUAOR_3+Wgd4QtP(xKB=e)2Tz9R9R-TL9~#wr$0C(!Kwa zsA6|PV}c}q75Sc6W|3t9&uI{A-U+!=%f5a4cJ$+u;D|7!P<+!|1Pf_*K%iZMVRkXP zPSReJA)au>dEkY=FZ%$;su*mkF8xKEprtTD)sXGn59+cIwO9S7$aYwPKMnyoTY}01 zyeFT!`Q&Gc3KBJ42{IvrZY2I6Z7%J`E^6-+T_F-la4{?5pQdl$F2PD#SX?}Rw&xv> zt;vw97Psc&>pNUpSD%nv+G?NiTL8qx2%9{PjBL#`QkdPO_55>oJ^8@Wx%q5a{(mLV z=wXtyh36A2J(<2mfGOe$5w;A3wQybW47qj`o-9BtVsPH2rgr}6({%*Wp*1nSB{4S- z?5X|Ta%i=Ze#_nC3h;Z!~zEmrA>GL@GBr7piD^47|!h*8d8CuP7DKh7!}(#%T2aj z(i~XL7uSB4PT#yL*WI1Smt>9#fNu_kCm?;cZxw7ebG9V~iER$tD_;B-BrTXkV%Z2T z1s~!|Cdp7A%ihQ>UJh?C@P#sFC84@AZsEpbix}3|y~V0_>C#wN{Yxbq*FxQEu3h3i z=js(3x@7$in}sg>-k`GJm()=CF?S^yE{uA)!XjQF1Vg)xLjTmv8o^&i)*@gBfZ-$#zZ(C`A>ZXj=Fv+G-zUW&AW&$t*P;<1# zX%_rxf2H|EV+~ACIw6&-+%W@@`N$^Uq>rhsKy13ZyMw?shwF&2y3_*%*vQW}q8Pdx zyB|(^94+&drje!Dg9mhn$(U3nr|NIY1a*x8!yRS%J?-s2Ab*Y$n7xD|Et8XT-KyEW zKfjA6#8VHF1ZLt`F|(}Jh6$s>ojk6QdIYb5`yk)vVmP~^^S2Q- zNTFd^)f4KAyM6ulC(=JWKSGXz>ahfbv=_fBrpYj=CP>;Jp@EXvKkKpv; z4_pQ|M$jI?jz8UHGRFfbg+P6PTV!vwmK^)tzo%n4tRj5e3PhjSvEyWe#Wiw?4LAQw z6CST;N|(tvyW4y=@^6&(O^0Lb0jPpcS?454NyS*!S5RC#A&|np7p-S{_usWC<3G2- z*(@ezoiI86pyJ`qhxpqtHB!`4E-sR6(Z7FqBKnu6Uq3%_{!L8B-V`;xdhXY^ujp7P z55Q=U8QNVr&(`xE$H+Pvp84s89r1un2$}v#20lIwB{Tck_}{L$H#lpq{Vp@*ojV_b zGQ*(?YFF1{wg)8A_v{TI?ti~VRt*6za&NXXg{_Tm_38=?u8@IG$A88hif;3BW~>|t z;~vhl0>iRQ>bMW)Ux~Uv^qX4?XWXbAgXc%J+=^!7O!v6! zt~=>nim7Y70{JbSC*fmy2nQpv2-@K+2bTq8D4%dI$ij@ch6qLFz+9QCk00D2^LdrX z6xCo~qqF5gFxULKv5N&+?fEkvUygma;8wV7YiIZ&=p;7vEdZ(FJge*JX@P);?N1iZxLL$L*TQg!_tqlbSh)ogVKM5KYDLhZaoxME zf-H4_@Pez?DnR16a{eZ9T)(a~s1th506y!CISgqdXpxhwYc@oo9jg9{u3+S=H;)<6 z8KCr6BF(4UhL_UnYiKZ$ju4DFF<+7N3};Rs$}-XMmtwlFr!n^$Ij97~%hU50EVwd} zzWevZ3l}Vj?Fh1|S@z-Fft&+Z2Cl(famYx~V5uxT?UUof#ekAQ2l<3B9g2i=31ull zL|S?&*gdFCp>;W=-lKi_3SJ7;WGC7eVh_NDwh+%AQf-n$hj#FH*z5n(gYR43RF1?= z3qu?ss(|1kAmQO45gjW@41^NE349BtXdC8=Z3jE`2F)2gF=a+3(eVq=#V_$CDGT<@ zXFM=ho!%422M|;XQ{A4cCkUfM)OTwF6A@Q0twe-e57j1s2X-Ksv9R9fPb3kdEv%5)F{S9lfDN7tGqqN1WjH853RRB9Vq zkDyXEc6Pn{3T_i;`PZ&pcXedsZ1zjlKU==&7TM5spR%1dD=I4P#o;5si!`80@(~Oq z>Xq-zp`m+#-jk@~*z^9tmwag9FcbMcNt2frX+j*B1MDC++pTy_*$i;F03j8ly9o)82B)ZN zKFi8gh*OOBY=iVahr(n9LD6_};k&jeD=X*2^bO88Gk1pi5`O-TXSv>tY4q6oG|Tgb zr+a&{<6gg920RXlwh+oP__lpdjB;FL&`<^3yh(?%BrsYn)Ga=h8hhflecdaewDor# z;>x={_25e;mJ&M*EHlioqb^uZ$^%7)45Q-6ml5pDM0C^S@C^{QA|>UsuwmnBrJng@ z=r1Ze9q`1ydy9w&(=AccSAc=HVC-l5xhe}wF*FKv^NHIspns#np zTIkWIVqp&XpBx~2<0rec!;NOneIhrd`{T88kM|NtvV9Pp58_e7m!*Q4RiuEK>FYdl z-V*HLNCAYTkv{WdDX{^AZ+&j{QLRD~-Fgy;5z1zz!)BKe+qZXR*vW|`#ESrUgj`P2 z(iZ1+8@1a1nG4XW*l6RNd!9MGud-i+H$Xul{YghcFxZAXT0I%GST&ODI44^{QC*AQ%n*HfK+~TI03< zFKc)%0}-_}8UHaz21uw1x6}K|$zS|W*JRIsIv$tbVzv6q^*`}y_ve&?f3H&hhL=a= z;hVvknJkO`@>=fRK%E{pejS?UwEV9>^^+unGk-}~C6?^NE>|LVnfIQHAnD^SM_J8)k{qE~*CL|Ojndq^ZVI3?AMdrtj#Q@=*TtO!Kc-%(jKoTT9 zh8WHY6G2B|>5l!1Ss;)a+ri?QfulmB$R{qo2-TV}K{)N<0=8QVNTgA_D}U0@Kseja z&n()4fH#D{!t|aJ-b{3)Lx&FwcUl}gSdngb5H>WyQt(`hI61x1fyHz?LSTURg^L{V zh1%!PQ8^D|ng+RUa#It(Gzo=(Jg9e$SXsyz{6AxJanVNdA@z8&4-_-JY~c5vueMqwdK=R z*Z;JntA+=F9G#Q;u3k0L@gwF}(Y6s;5w?ydcy!2cV#;f$?jJv5hh}Z_+D`ZM1okCs z-Nuw5M7fV9vm)uPxX(8k>>0uXT~y6v{_tOA&@W{{o;KkuI;!t~BEfF4%(M(x3=T$n z84CMe=i*8BVU%Y=(*Xw)r5v2F_oCG*5AZmBTXkU|AcGuOTr$K!P8%Qt3t$76*!~vT z5H8DbqQ-t9hdb#QxoWO4buR)1h4W9CQ5Gv(`W1rnD^Z^_+YzgWG{# zW@@6Mhz&Em30DSjjKe|t&)+K224@PieEsfSrsd1;oM--?ZP~WycI}{Nfam4XGoe3g zK20Bwdo>L9-4nHhJWg18y)jb{FB1(=0F{TlD{fi3m_#BN@Qa{mi7Oaqe31dt|1Q9N zUD2iTA0>>TKX>h-C(_UF+BozzZHEixEBr{Xt0}6e&;dp=`M#0isp zcp=0*e+(%ZkDr7Nvr^tATH0;2?bdK+Tb7{1%yDg`1o-&M@lx$QLSDV^efHx`C_w}j zZan#UDXLX5c=PyZ=%BJ_!7hRwC1Rh7$`c(WAUxYMXR1+7{yk|bVnX0Xn_JVnz{0d^ zu$%j5Y7&JDjONo5V9p;(K}P?hZ9(6M6V!_gcop8M`rGRsdi~8Qh;4t%F#qYql`EFQ zxNkn@XzX2AnSd?qgSFTK#5A$B6W{|CKyfIS*2wKEo8s)gn0WM+tpK?pYWHU#%~SuN zCIppztfs2!cFMdlAWcJ#up=+Q zFosZWMeS@RP5O*8lMJ44VVdDx7_aku@xnP(iu8)i%$KkyiL(u7-^8`#;4G@C$zfBt z%a6LAG_jpI%nkc&5nLKbqw7hWU?A@m)IGwl;2a)!?V7;&QVskGzr*Gd9`8zgw6By+ z?-9KWdrN|}D9jZ2Lds}Dkda(LjxZq{fZ|0)Ttf91=kPi2XPhu(aZ8UVZdk?t(b(R~ahMuT_f$(I=@?l0I;14WN(6-K zxo2dH5@S`iqfMIl=FP>3(&xTAOfl#F5b?!HL6ZILU&m z8MdA}(e>j86NZUhe4g~(P_k1!u;e;i2cK$dzoJShkInaK*rlrKdFPH%YH^TuMPZVN ziS+%V1;xo{zn)zp%3aocvBfaURu*#)PX8X^kT&23^A;-PJ?pLb=;6Af+0xBSv9hIO z;YleelVE1Xx4^cVF1^U0&-#mVU!+~Sgw2QAdpmA6{XwscBG zI0Ildhmn&ff_Gv!63!<_K|XNcLW`k`t;m1eCW`j?cUqBkjsMmOw47Z?>zGb2QS<$+!mN*c=(`j)r#3Q2y^V%kw>e~ zoHP1uc@RK9aBehCsljZPVL5LABc32(g#@(9XK=yUwr9~UVgth~A*wrx3 z;N%<8JRemKoT5l<*= z3mEzbII{%aJ!PD}C;w%hCWp_C5UICI-o>OXnFa#NW12j4B!y?&VnT>YOG5Xt*$vH4lu;Q3ij$c zPueycEFC$y2cw4;@WDwt4~c-z^h-~dhVf57W<_zbZF%c=NhCQVTO6^APU`T}ma|Kk zZ~J`*kyM^&zJri>NWJN2`}xR8KS+AY%dv#o+phg_ndnrSB`h0V$@8 z=1#BrO`AQY(?uX$Jr-0~Q)9z^ZpML4iu6~HXyNd?+Gol!8aI;0@wn$vcAZs1%6GsMGN=Z=1oGdM(*W1%#pLAV zblb1zS)1SWeg_2cfqRSr@;o_*FzIxcKB7<%ma26)SMMT7xysWW3Nrpj_FxYSQ9huo z=y|2@UkuLd&vNX&Khpm2+DjCaWY>74fIUxMCBv0RPTCYMYx#bF@1rzL*Ln;hyaJd7fM*opA}YP2m&OvoL4Owm^~+c2?Ml2@P>p7e(yc>1yeRfNK|&+b(^R>jL?x2MxGz9 zWuuCd1wS16fm_#tfCaF+vqAvL3g&E7N2T~0Rz)+MtVP&0I4jMgxKaUZ&OMTTlqFHw!i!1(_0ZgK$~SuMR^5HLL!X7er`q}Df;}7l*CiG0y&Kw z6$!5ZC&GyXP6Rd-6AKXT*nC%4$_Z0G*tq{+V`m-)Wm4)hA6ufW!Gj%XKaa%gsV3-4Oym16BX%vo>z0uT-TZF zI>%pg@fy9q-}n1`@8^E*`~LRZRfKL|qDnV6`h|MpXmBC`AcoY2{lV|1+|{Ev0LccJ z*moir%_iQ&EacT5L6iAD%fEQ>Goo`nUeprN+K(CeV8(vE zsM!bIJW-7A->&1Hy{NDAj(z=N}`hQz8$J8ziTlkAnh;a zlxFMt(`$;b>W{>kpE6JWj#gVk^Z{AFyseJ-unn+(4d+%8WxDvO`rR|PyLG|A!KPP{ zVk_bEQuYFO#iLEShC_(*q;=k~CRAIjJZ%$5jT~UZ{J1NOCB%(0%D>Rw-nq*&ph8YZ+9h!h`b#^jkp}a z3W@AkL>$b>dda>tcfNA7xY*)>^#WYH)c_ZdJYsTxIg8j@l9}g)PRU+W|7KKGTKsh6 zL!aTi5EI~6;GS|!0F#}ZB$U@6VFT&otN9{(DfJ2)#a>c|e0(-Nd;DoLxLCr83Cxk; z!PI_)5Z=oUv!;7(J=EOQPG=RWO=(6(FD+Gw+}`{&GgBfBl7}B)UL!f;Ym=GDuNWQBf7L5-Scv`60^xHuP z-W&7It)DMRBs50U25-vTdA;3f-;nTqdtkrP8=gzj$Nd~=6+4(Ggf7V5-kuPbZxe5j zq2FWcHC6V?&{K_P5LzM-$fJs_M|?HademnQJSm`_WK7CbZod?|f}_)ikPfM;tl7v4 zCEqb=@|Xat*!Ik%3y;m3{zCJRT;m*d)Mm`Sc?sGwqTno$krdiR#?EK<*d`g%IsM5G zh!b8Zz41#rFjDj(jDNvyx*;JUphYR#&>oCO+XcQ*<7nlCoWKM2!Uaizdse@GoCl6; z2ge8{2k^+ca@(}bDs~HGM-HFnAx~r?Cg+qmfdoQmWz-K)^U5V15N}^cbPn6dK?UR> zE_~zhCKc%+MyQSilAzIsLwWjo&A6o)qAnu2c9F>fS^1)i;7~2VxS<&5|2U9Rik5Ff zSuHO>JpZ)hNo-8RxY-XME$U+RNF zuL6zoWox6$qdMn;K^bW2Cv1Q_gSeqrn7{|X^L~!w4ULR0u@4dBD0MaN+kPIoJ7;IN z8-NmVril&rc}b=b;Z&2DP$~lMqIHv(fBBc0P%*VRZfAX7JpOZgsRY&hu1}u)iiXOA zgbYtYuggF^rFw)6uCeiSW}8Nk+BIKWe<%w?qW+}CD`@%Y=r6HFA4k{!KupnGC~F`S{RmGh0990dn%;f;1H z=koC6;sVADO3CwMHK&tJ%yOy+4jf)zU)WMmvyYV}uLU0rwWS44YJPZak@FC{>JspY zbycSdr%o3BBmYoIPH>yNRPX-|1J^W%|MS`n&8Pn2K8{3hr+5>Y4Sx9X#8q|QRqcY> zXin-sv=-4PMjlvU0y;#ThbE%>lAblUPY@ql_gha%s8nS;)jh#YAR%*}x4fP>L5gB) zo};qL9U_oCh`;fLIxCK~w3mn7b>!l%lBz6B@qG(R%S(lYCzVfoJNx?h zG&?k+x@fv)yYC=Te%Q6Ex?oaw$@bnh)T_m5q+l{o2VcZ|y(IrgFkvw415H=CefwV2 zX?zbv=x~cs$4WxBIN7J z+r7n8Dm|@xnhCvDIAH{Kp?94Q(`3l-TUhfMGq$;US5kmP+bWliu+W)?C$aXq{Nj4` z4%)?BK#KtW+~jirriWRfM)4*zChdNE{W=G!o)igv?=WPA#N~5OcWn3NqprPkx_16DtjATkLqb z2mmGKkIA=H$g8$^5u~$)?@d0NKv_5%s+P&;m#Z}I-8&KKqIAnNl0VFAq{190zV$6t zlLP1yZMOcpBYZP01-Ew`K}@Je4;)BDVIqg1AZeTjdq@f9q)TyCOU@~ec`3=!vXLdO zL|Ca%I<#^i{JqouMiPyHVk0*&57d(ccPpz*Ob*F2PqmgAIyKj=k_Fi zT@FQQa|3I85DlHQ`_lbWpC*AYlv3nOd3frN6TL3c?j*NqqhdFB3P6PobcKBlMfQOnTyp~WAonB^AMwu z9iW(d|D%&PCJ)t*v$4?9x=azU<99c%{9I6hv|$TLSj)$!cZpB8wYEm!&GLyBEwDWE$&7^q5`%M z+vQMtu%VWCym54P&IPBdkXwlxUi^lE6%MDn&Cp~i^|p@yetG-drXr}0P8&qUc^nic zzI+%GYL%<8`>+*+VQwt9kPHW?bJYoNJBd!aa-|iWS8>@@Dv^QcM7C_%5@XKKh-WX? z$xIQyI}qF;b+(6x{%Zzs+F+2vl-aXXo=C`Hmnwpsou#?0JcZvDioO&Q9i+rS#S25j zLUq;ha^ii4&BfB1r4 zXprUJRtm?~CG0&c16`zz&}ZIwA!lZ)S@KebKxrgJMTxRlJ_+L9xMN3aTU%S0i;4YJ z+6r$=3G6)l8kjoCcDYG&$8qugh;xuJyZjcr^;9bV+%{F z1!lIs%3PuoxbQ+h>+MTN8lsN!%pi63btN)}#F)-9*CDDtv8dZZ86>!Cvjds83Sq2rk7MajpJ{2OsD%x#aY9R~Rf!M2B+m8YHr0PceW7!{z;KAZN<%|HM&tG+6lX nvfy&fum2kR{QD11HEEY#9CGdU(>)+xMX|S=G9krgZp6O;`D}Pg literal 0 HcmV?d00001 diff --git a/tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_1d.png b/tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_1d.png new file mode 100644 index 0000000000000000000000000000000000000000..38ed5f00ae50490ea1c9484f27748011d8b19766 GIT binary patch literal 19189 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 USC?8hQO3aW=`2Txx6U)zMAZ2$lO literal 0 HcmV?d00001 diff --git a/tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_2d.png b/tests/baseline_images/tests.transect.test_plot.test_plot_no_intersection_2d.png new file mode 100644 index 0000000000000000000000000000000000000000..eeac04037ec6d0f870fa77dde335908779950c66 GIT binary patch literal 26461 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 literal 0 HcmV?d00001 diff --git a/tests/transect/__init__.py b/tests/transect/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/transect/test_plot.py b/tests/transect/test_plot.py index 863cb4a..babe9b1 100644 --- a/tests/transect/test_plot.py +++ b/tests/transect/test_plot.py @@ -17,15 +17,20 @@ @pytest.mark.mpl_image_compare @pytest.mark.matplotlib @pytest.mark.tutorial -def test_plot( +def test_plot_2d( datasets: pathlib.Path, tmp_path: pathlib.Path, ): dataset = emsarray.tutorial.open_dataset('gbr4') dataset = estimate_bounds_1d(dataset, 'zc') - temp = dataset['temp'].copy() + temp = dataset['temp'] temp = temp.isel(time=-1) + # Unfortunately the bathymetry variable 'botz' and the depth coordinate 'zc' + # have different `positive` conventions. + dataset['botz'] = -dataset['botz'] + dataset['botz'].attrs['positive'] = dataset['zc'].attrs['positive'] + line = shapely.LineString([ [152.9768944, -25.4827962], [152.9701996, -25.4420345], @@ -62,10 +67,54 @@ def test_plot( return figure +@pytest.mark.mpl_image_compare +@pytest.mark.matplotlib +@pytest.mark.tutorial +def test_plot_1d( + datasets: pathlib.Path, + tmp_path: pathlib.Path, +): + dataset = emsarray.tutorial.open_dataset('gbr4') + dataset = estimate_bounds_1d(dataset, 'zc') + eta = dataset['eta'] + eta = eta.isel(time=-1) + eta.attrs['units'] = 'metres' + + line = shapely.LineString([ + [152.9768944, -25.4827962], + [152.9701996, -25.4420345], + [152.9727745, -25.3967620], + [152.9623032, -25.3517828], + [152.9401588, -25.3103560], + [152.9173279, -25.2538563], + [152.8962135, -25.1942238], + [152.8692627, -25.0706729], + [152.8623962, -24.9698750], + [152.8472900, -24.8415806], + [152.8308105, -24.6470172], + [152.7607727, -24.3521012], + [152.6392365, -24.1906056], + [152.4792480, -24.0615124], + ]) + emsarray.transect.plot( + dataset, line, eta) + + figure = matplotlib.pyplot.gcf() + axes = figure.axes[0] + # This is assembled from the variable long_name and the time coordinate + assert axes.get_title() == 'Surface elevation\n2022-10-15T14:00' + # This is the long_name of the depth coordinate + assert axes.get_ylabel() == 'metres' + # This is made up + assert axes.get_xlabel() == 'Distance along transect' + + return figure + + @pytest.mark.mpl_image_compare @pytest.mark.matplotlib(mock_coast=True) @pytest.mark.tutorial -def test_plot_no_intersection( +def test_plot_no_intersection_2d( datasets: pathlib.Path, tmp_path: pathlib.Path, ): @@ -104,3 +153,43 @@ def test_plot_no_intersection( assert colorbar.get_ylabel() == 'degrees C' return figure + + +@pytest.mark.mpl_image_compare +@pytest.mark.matplotlib(mock_coast=True) +@pytest.mark.tutorial +def test_plot_no_intersection_1d( + datasets: pathlib.Path, + tmp_path: pathlib.Path, +): + """ + Transects that do not intersect the dataset geometry need special handling. + This should produce an empty transect plot, which is better than raising an error. + """ + dataset = emsarray.tutorial.open_dataset('gbr4') + dataset = estimate_bounds_1d(dataset, 'zc') + + eta = dataset['eta'].copy() + eta = eta.isel(time=-1) + eta.attrs['units'] = 'metres' + + # This line goes through the Bass Strait, no where near the GBR. + # Someone picked the wrong dataset... + line = shapely.LineString([ + [142.097168, -39.206719], + [145.393066, -39.3088], + [149.798584, -39.172659], + ]) + emsarray.transect.plot( + dataset, line, eta) + + figure = matplotlib.pyplot.gcf() + axes = figure.axes[0] + # This is assembled from the variable long_name and the time coordinate + assert axes.get_title() == 'Surface elevation\n2022-10-15T14:00' + # This is the long_name of the depth coordinate + assert axes.get_ylabel() == 'metres' + # This is made up + assert axes.get_xlabel() == 'Distance along transect' + + return figure diff --git a/tests/transect/test_transect.py b/tests/transect/test_transect.py new file mode 100644 index 0000000..0aa5af6 --- /dev/null +++ b/tests/transect/test_transect.py @@ -0,0 +1,98 @@ +import pytest +import shapely +from matplotlib.figure import Figure + +import emsarray +from emsarray.transect import Transect +from emsarray.transect.artists import CrossSectionArtist, TransectStepArtist +from emsarray.utils import estimate_bounds_1d + +# Testing the intersection data of the transect is difficult +# without simply hard coding the expected intersections. +# Instead we mostly test that the transect functions return reasonably shaped things, +# and rely on the plot tests when comparing actual results. + +# This line stays within the bounds of the GBR4 dataset. +LINE_INBOUNDS = shapely.LineString([ + [150.172296, -19.1132187], + [150.985268, -20.7740421], + [151.657324, -22.0254994], + [152.784644, -23.3156906], + [153.478380, -24.5738101], +]) + +# This line starts inside the dataset, exits to the north east in a big spike, +# then comes back in bounds again. +LINE_IN_OUT = shapely.LineString([ + [150.172296, -19.1132187], + [150.985268, -20.7740421], + [157.228887, -19.1951146], + [152.784644, -23.3156906], + [153.478380, -24.5738101], +]) + +# This line is completely outside the dataset and never intersects. +LINE_OUT_OF_BOUNDS = shapely.LineString([ + [154.414304, -15.7909029], + [156.842378, -18.3706571], + [158.533358, -21.2663872], + [159.129537, -23.2525561], +]) + + +@pytest.mark.tutorial +def test_transect(): + dataset = emsarray.tutorial.open_dataset('gbr4') + transect = Transect(dataset, LINE_IN_OUT) + + # A whole bunch of segments + assert len(transect.segments) > 0 + assert transect.intersection_bounds.shape == (len(transect.segments), 2) + assert transect.linear_indexes.shape == (len(transect.segments),) + + # The path exits and re-enters the dataset once, so one hole + assert transect.holes.shape == (1,) + + assert len(transect.points) == len(LINE_IN_OUT.coords) + + # Extract temp data, which + temp_data = transect.extract('temp') + assert temp_data.dims == ('time', 'k', 'index') + assert temp_data.sizes['index'] == len(transect.segments) + + eta_data = transect.extract('eta') + assert eta_data.dims == ('time', 'index') + assert eta_data.sizes['index'] == len(transect.segments) + + botz_data = transect.extract('botz') + assert botz_data.dims == ('index',) + assert botz_data.sizes['index'] == len(transect.segments) + + +@pytest.mark.matplotlib +@pytest.mark.tutorial +def test_transect_make_artist_cross_section(): + dataset = emsarray.tutorial.open_dataset('gbr4') + dataset = estimate_bounds_1d(dataset, 'zc') + transect = Transect(dataset, LINE_INBOUNDS) + + figure = Figure(figsize=(12, 3)) + axes = figure.add_subplot() + artist = transect.make_artist(axes, dataset['temp'].isel(time=0)) + + assert isinstance(artist, CrossSectionArtist) + assert artist in axes._children + + +@pytest.mark.matplotlib +@pytest.mark.tutorial +def test_transect_make_artist_transect(): + dataset = emsarray.tutorial.open_dataset('gbr4') + transect = Transect(dataset, LINE_INBOUNDS) + + figure = Figure(figsize=(12, 3)) + axes = figure.add_subplot() + artist = transect.make_artist(axes, dataset['eta'].isel(time=0)) + + assert isinstance(artist, TransectStepArtist) + assert artist in axes._children From 3988ba45c7a31a91df77bdee54019e577dfd6b6a Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Thu, 26 Mar 2026 11:54:10 +1100 Subject: [PATCH 3/3] Fix style issues --- src/emsarray/transect/__init__.py | 1 - src/emsarray/transect/base.py | 4 ++-- src/emsarray/transect/utils.py | 10 ++++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/emsarray/transect/__init__.py b/src/emsarray/transect/__init__.py index 0e91cca..3cc522a 100644 --- a/src/emsarray/transect/__init__.py +++ b/src/emsarray/transect/__init__.py @@ -1,7 +1,6 @@ from .base import Transect, TransectPoint, TransectSegment from .utils import plot, setup_depth_axis, setup_distance_axis - __all__ = [ 'Transect', 'TransectPoint', 'TransectSegment', 'plot', 'setup_depth_axis', 'setup_distance_axis', diff --git a/src/emsarray/transect/base.py b/src/emsarray/transect/base.py index 887c480..7e2a25c 100644 --- a/src/emsarray/transect/base.py +++ b/src/emsarray/transect/base.py @@ -17,7 +17,6 @@ from . import artists - # Useful for calculating distances in a AzimuthalEquidistant projection # centred on some point: # @@ -525,7 +524,8 @@ def make_transect_step_artist( """ data_array = name_to_data_array(self.dataset, data_array) if edgecolor == 'auto': - edgecolor = axes._get_lines.get_next_color() + cycler: Any = axes._get_lines # type: ignore + edgecolor = cast(ColorType, cycler.get_next_color()) artist = artists.TransectStepArtist.from_transect( self, data_array=data_array, fill=fill, edgecolor=edgecolor, **kwargs) diff --git a/src/emsarray/transect/utils.py b/src/emsarray/transect/utils.py index 8c64a4f..d998bf0 100644 --- a/src/emsarray/transect/utils.py +++ b/src/emsarray/transect/utils.py @@ -13,6 +13,7 @@ from emsarray.plot import make_plot_title from emsarray.types import DataArrayOrName, Landmark from emsarray.utils import name_to_data_array + from .base import Transect @@ -77,6 +78,7 @@ def plot( setup_distance_axis(transect, axes) if depth_coordinate is not None: + ylim: tuple[float, float] | bool if len(transect.segments) > 0: depths_with_data = numpy.flatnonzero(numpy.isfinite(transect_data.values).any(axis=-1)) depth_bounds = dataset[depth_coordinate.attrs['bounds']] @@ -85,7 +87,7 @@ def plot( depth_bounds.values[depths_with_data[-1], 1], ) else: - ylim = None + ylim = False setup_depth_axis(transect, axes, depth_coordinate=depth_coordinate, ylim=ylim) if bathymetry is not None: @@ -181,12 +183,12 @@ def setup_depth_axis( axes.set_ylim(depth_max, depth_min) else: axes.set_ylim(depth_min, depth_max) - elif ylim not in {False, None}: + elif ylim is not False: axes.set_ylim(ylim) if label is True: - label = depth_coordinate.attrs.get('long_name') - if label not in {False, None}: + label = str(depth_coordinate.attrs.get('long_name')) + if label is not None and label is not False: axis.set_label_text(label) if units is True: