Coordinates and Coordinate Managers

Open in JupyterLite
Warning

This page covers advanced features. Most users should work through the Patch coordinate interface.

BaseCoord represents one coordinate. CoordManager groups coordinates and keeps their associated arrays aligned. Both return new objects rather than modifying themselves.

Coordinates

Creation

get_coord chooses the appropriate coordinate type from a range or array.

import re

import numpy as np

from dascore.core import get_coord, get_coord_manager

regular = get_coord(start=0, stop=10, step=1, units="m")
same = get_coord(data=np.arange(10), units="m")
irregular = get_coord(data=np.array([0.0, 1.0, 2.2, 4.0]))

assert regular == same

Coordinates expose values, units, data type, ordering, sampling regularity, limits, and length through their attributes and methods.

Attribute Meaning
sorted Whether the coordinate reports ascending ordering
reverse_sorted Whether the coordinate reports descending ordering
evenly_sampled Whether one step describes every value
dtype NumPy data type
data Array of coordinate values
units Physical units, if present
degenerate Whether the coordinate is empty or zero-dimensional
min(), max() Coordinate limits

Segmented coordinates

CoordSegmented preserves monotonic blocks and their discontinuities. Use concat_coords(...) when the blocks are known:

from dascore.core.coords import concat_coords

first = get_coord(start=0.0, stop=5.0, step=1.0, units="m")
second = get_coord(start=8.0, stop=13.0, step=1.0, units="m")
segmented = concat_coords(first, second)

print(segmented.get_discontinuities("gaps"))
   index  before  after  delta  excess
0      5     4.0    8.0    4.0     3.0

Use CoordSegmented.from_array(...) to preserve an exact monotonic array. It returns the simplest truthful coordinate, so uniform input becomes CoordRange; segmented output has step=None. simplify(tolerance=...) may reduce segments within an error bound, while snap() replaces values with one evenly sampled range. Use patch.split_gaps() before writing segmented dimensions to formats that cannot represent gaps.

from dascore.core.coords import CoordSegmented

values = np.array([0.0, 1.0, 2.0, 7.0, 8.0, 9.0])
exact = CoordSegmented.from_array(values, tolerance=0, units="m")
assert np.array_equal(exact.values, values)

Updating and units

update uses an existing coordinate as a template:

start = np.datetime64("2023-01-01")
coord = get_coord(start=start, stop=start + np.timedelta64(1, "h"), step=np.timedelta64(1, "m"))

shifted = coord.update(data=coord.values + np.timedelta64(10, "s"))
coarser = coord.update(step=np.timedelta64(30, "m"))

Updating min, max, or step retains the coordinate length and derives the remaining range values. Updating data instead lets DASCore choose the simplest coordinate class that represents the new array.

set_units changes the label without changing values; convert_units converts both.

relabeled = regular.set_units("ft")
converted = regular.convert_units("ft")

Sorting, snapping, and selecting

Methods that reorder or remove samples return both a coordinate and an indexer for the associated array.

unsorted = get_coord(data=np.array([3, 1, 2]))
sorted_coord, indexer = unsorted.sort()

data = np.arange(6).reshape(3, 2)
sorted_data = data[indexer, :]

selected_coord, indexer = regular.select((3, 7))
selected_data = np.arange(len(regular))[indexer]

Selections accept compatible quantities and convert them before comparison:

from dascore.units import ft

selected_in_feet, _ = regular.select((3 * ft, 20 * ft))

sort permutes existing values and returns the matching permutation. snap changes values, while simplify(tolerance=...) chooses a simpler representation only within the requested displacement bound. The distinction is important whenever coordinate values are measurements rather than ideal labels.

snap() returns only a coordinate because it replaces values rather than permuting them. Use CoordManager.snap or Patch.snap_coords when data must be sorted with the coordinate before snapping.

String coordinates

String coordinates support exact matches, * and ? wildcards, compiled regular expressions, boolean masks, and sorting. They do not support units, relative selection, or numeric ranges.

labels = get_coord(data=np.array(["ch_a", "ch_b", "aux_1", "aux_2"]))

exact, _ = labels.select("ch_b")
channels, _ = labels.select("ch_*")
aux, _ = labels.select(re.compile(r"^aux_\d$"))
sorted_labels, sort_indexer = labels.sort()

Strings containing * or ? are treated as wildcard patterns, so avoid those characters in labels which must be matched literally. Boolean masks are the unambiguous choice for arbitrary sets.

Finding insertion positions

get_next_index returns where a value is or would be inserted in a sorted coordinate:

assert regular.get_next_index(2) == 2
assert regular.get_next_index(2.1) == 3

Coordinate managers

Creation and association

get_coord_manager creates a manager from coordinates and ordered dimensions. Tuple values specify (associated_dimensions, coordinate); use None for an independent coordinate.

distance = get_coord(start=0, stop=10, step=1)
time = get_coord(start=0, stop=1, step=0.1)
latitude = get_coord(data=np.linspace(41.5, 41.6, len(distance)))

manager = get_coord_manager(
    coords={
        "distance": distance,
        "time": time,
        "latitude": ("distance", latitude),
        "deployment": (None, get_coord(data=np.array(["A"]))),
    },
    dims=("distance", "time"),
)

manager.dims stores dimension order, coord_map maps names to coordinates, and dim_map records dimensional associations.

print(manager.dims)
print(dict(manager.dim_map))
('distance', 'time')
{'distance': ('distance',), 'time': ('time',), 'latitude': ('distance',), 'deployment': ()}

Updating

CoordManager.update replaces, adds, disassociates, or drops coordinates:

new_distance = distance.update(data=distance.values + 10)
updated = manager.update(distance=new_distance)
detached = updated.update(latitude=(None, latitude))
without_latitude = detached.update(latitude=None)

New coordinates use the same update form:

quality = get_coord(data=np.linspace(0, 1, len(distance)))
with_quality = manager.update(quality=("distance", quality))
independent = manager.update(run=(None, get_coord(data=np.array([1]))))

Setting a dimension coordinate to None drops that dimension and every coordinate depending on it. Dropping a missing non-dimensional coordinate is a no-op.

Methods with data

Operations that change array alignment accept the array and return an updated manager and array. Coordinate names are keyword arguments, as on Patch.

import dascore as dc

patch = dc.get_example_patch()
manager, data = patch.coords, patch.data

selected_manager, selected_data = manager.select(data=data, distance=(..., 100))
sorted_manager, sorted_data = manager.sort("time", "distance", array=data, reverse=True)
renamed = manager.rename_coord(time="elapsed_time")

When several dimensions are sorted together, the returned data reflects each permutation in dimension order. Renaming a dimension also updates every coordinate association that refers to it.

select, order, sort, and snap accept an associated array and return (manager, array). Methods that do not change alignment, such as unit conversion, return only a manager.

Prefer the corresponding Patch methods when operating on a patch; they keep data, coordinates, metadata, and provenance together.