# ======================================================================================
# Copyright (©) 2015-2025 LCS - Laboratoire Catalyse et Spectrochimie, Caen, France.
# CeCILL-B FREE SOFTWARE LICENSE AGREEMENT
# See full LICENSE agreement in the root directory.
# ======================================================================================
"""Module that implements class `CoordSet` ."""
__all__ = ["CoordSet"]
import copy as cpy
import uuid
import warnings
import numpy as np
from traitlets import All
from traitlets import Bool
from traitlets import Dict
from traitlets import HasTraits
from traitlets import Int
from traitlets import List
from traitlets import Unicode
from traitlets import default
from traitlets import observe
from traitlets import signature_has_traits
from traitlets import validate
from spectrochempy.core.dataset.baseobjects.ndarray import DEFAULT_DIM_NAME
from spectrochempy.core.dataset.baseobjects.ndarray import NDArray
from spectrochempy.core.dataset.coord import Coord
from spectrochempy.utils.misc import is_sequence
from spectrochempy.utils.print import colored_output
from spectrochempy.utils.print import convert_to_html
# ======================================================================================
# CoordSet
# ======================================================================================
[docs]
@signature_has_traits
class CoordSet(HasTraits):
r"""
A collection of Coord objects for a NDArray object with validation.
This object is an iterable containing a collection of Coord objects.
Parameters
----------
*coords : `NDArray` , `NDArray` subclass or `CoordSet` sequence of objects.
If an instance of CoordSet is found, instead of an array, this means
that all coordinates in this coords describe the same axis.
It is assumed that the coordinates are passed in the order of the
dimensions of a nD numpy array (
`row-major <https://docs.scipy.org/doc/numpy-1.14.1/glossary.html#term-row-major>`_
order), i.e., for a 3d object : 'z', 'y', 'x'.
**kwargs
Additional keyword parameters (see Other Parameters).
Other Parameters
----------------
x : `NDArray` , `NDArray` subclass or `CoordSet`
A single coordinate associated to the 'x'-dimension.
If a coord was already passed in the argument, this will overwrite
the previous. It is thus not recommended to simultaneously use
both way to initialize the coordinates to avoid such conflicts.
y, z, u, ... : `NDArray` , `NDArray` subclass or `CoordSet`
Same as `x` for the others dimensions.
dims : list of string, optional
Names of the dims to use corresponding to the coordinates. If not
given, standard names are used: x, y, ...
copy : bool, optional
Perform a copy of the passed object. Default is True.
See Also
--------
Coord : Explicit coordinates object.
NDDataset: The main object of SpectroChempy which makes use of CoordSet.
Examples
--------
>>> from spectrochempy import Coord, CoordSet
Define 4 coordinates, with two for the same dimension
>>> coord0 = Coord.linspace(10., 100., 5, units='m', title='distance')
>>> coord1 = Coord.linspace(20., 25., 4, units='K', title='temperature')
>>> coord1b = Coord.linspace(1., 10., 4, units='millitesla', title='magnetic field')
>>> coord2 = Coord.linspace(0., 1000., 6, units='hour', title='elapsed time')
Now create a coordset
>>> cs = CoordSet(t=coord0, u=coord2, v=[coord1, coord1b])
Display some coordinates
>>> cs.u
Coord: [float64] hr (size: 6)
>>> cs.v
CoordSet: [_1:temperature, _2:magnetic field]
>>> cs.v_1
Coord: [float64] K (size: 4)
"""
# Hidden attributes containing the collection of objects
_coords = List(allow_none=True)
_references = Dict()
_updated = Bool(False)
# Hidden id and name of the object
_id = Unicode()
_name = Unicode()
# Hidden attribute to specify if the collection is for a single dimension
_is_same_dim = Bool(False)
# other settings
_copy = Bool(False)
_sorted = Bool(True)
_html_output = Bool(False)
# default coord index
_default = Int(0)
# ----------------------------------------------------------------------------------
# initialization
# ----------------------------------------------------------------------------------
def __init__(self, *coords, **kwargs):
self._copy = kwargs.pop("copy", True)
self._sorted = kwargs.pop("sorted", True)
keepnames = kwargs.pop("keepnames", False)
# if keepnames is false and the names of the dimensions are not passed in kwargs, then use dims if not none
dims = kwargs.pop("dims", None)
self.name = kwargs.pop("name", None)
# initialise the coordinate list
self._coords = []
# First evaluate passed args
# --------------------------
# some cleaning
if coords:
if all(
(
isinstance(coords[i], np.ndarray | NDArray | list | CoordSet)
or coords[i] is None
)
for i in range(len(coords))
):
# Any instance of a NDArray can be accepted as coordinates for a dimension.
# If an instance of CoordSet is found, this means that all
# coordinates in this set describe the same axis
coords = tuple(coords)
elif is_sequence(coords) and len(coords) == 1:
# if isinstance(coords[0], list):
# coords = (CoordSet(*coords[0], sorted=False),)
# else:
coords = coords[0]
if isinstance(coords, dict):
# we have passed a dict, postpone to the kwargs evaluation process
kwargs.update(coords)
coords = None
else:
raise ValueError("Did not understand the inputs")
# now store the args coordinates in self._coords (validation is fired when this attribute is set)
if coords:
for coord in coords[::-1]: # we fill from the end of the list
# (in reverse order) because by convention when the
# names are not specified, the order of the
# coords follow the order of dims.
if not isinstance(coord, CoordSet):
if isinstance(coord, list):
coord = CoordSet(*coord[::-1], sorted=False)
else:
coord = Coord(coord, copy=True)
else:
coord = cpy.deepcopy(coord)
if not keepnames:
if dims is None:
# take the last available name of available names list
coord.name = self.available_names.pop(-1)
else:
# use the provided list of dims
coord.name = dims.pop(-1)
self._append(coord) # append the coord (but instead of append,
# use assignation -in _append - to fire the validation process )
# now evaluate keywords argument
# ------------------------------
for key, coord in list(kwargs.items())[:]:
# remove the already used kwargs (Fix: deprecation warning in Traitlets - all args, kwargs must be used)
del kwargs[key]
# prepare values to be either Coord or CoordSet
if isinstance(coord, list | tuple):
coord = CoordSet(
*coord,
sorted=False,
) # make sure in this case it becomes a CoordSet instance
elif isinstance(coord, np.ndarray) or coord is None:
coord = Coord(
coord,
copy=True,
) # make sure it's a Coord # (even if it is None -> Coord(None)
elif isinstance(coord, str) and coord in DEFAULT_DIM_NAME:
# may be a reference to another coordinates (e.g. same coordinates for
# various dimensions)
self._references[key] = coord # store this reference
continue
# Populate the coords with coord and coord's name.
if isinstance(coord, NDArray | Coord | CoordSet):
if key in self.available_names or (
len(key) == 2
and key.startswith("_")
and key[1] in list("123456789")
):
# ok we can find it as a canonical name:
# this will overwrite any already defined coord value
# which means also that kwargs have priority over args
coord.name = key
self._append(coord)
elif not self.is_empty and key in self.names:
# append when a coordinate with this name is already set in passed
# arg.
# replace it
idx = self.names.index(key)
coord.name = key
self._coords[idx] = coord
else:
raise KeyError(
f"Probably an invalid key (`{key}` ) for coordinates has been passed. "
f"Valid keys are among:{DEFAULT_DIM_NAME}",
)
else:
raise ValueError(
f"Probably an invalid type of coordinates has been passed: {key}:{coord} ",
)
# store the item (validation will be performed)
# self._coords = _coords
# inform the parent about the update
self._updated = True
# set a notifier on the name traits name of each coordinates
for coord in self._coords:
if coord is not None:
HasTraits.observe(coord, self._coords_update, "_name")
# initialize the base class with the eventual remaining arguments
super().__init__(**kwargs)
@staticmethod
def _implements(name=None):
"""
Check if the current object implements `CoordSet`.
Rather than isinstance(obj, CoordSet) use object._implements('CoordSet').
This is useful to check type without importing the module.
"""
if name is None:
return "CoordSet"
return name == "CoordSet"
# ----------------------------------------------------------------------------------
# Special methods
# ----------------------------------------------------------------------------------
def __sub__(self, other):
"""
Subtraction of Coordsets.
Parameters
----------
other : CoordSet
The Coordset to subtract to self.
Returns
-------
sub : CoordSet
The difference of the Coordsets.
"""
out = []
if isinstance(other, CoordSet):
for coord1, coord2 in zip(self.coords, other.coords, strict=False):
out.append(coord1 - coord2)
else:
raise NotImplementedError(
f"Subtraction f a CoordSet with an object of type {type(other)} is not implemented yet"
)
return CoordSet(list(out))
def __add__(self, other):
"""
Addition of Coordsets.
Parameters
----------
other : CoordSet,
The Coordset to add to self.
Returns
-------
add : CoordSet
The sum of the Coordsets.
"""
out = []
if isinstance(other, CoordSet):
for coord1, coord2 in zip(self.coords, other.coords, strict=False):
out.append(coord1 + coord2)
else:
raise NotImplementedError(
f"Addition of a CoordSet with an object of type {type(other)} is not implemented yet"
)
return CoordSet(list(out))
# ----------------------------------------------------------------------------------
# Validation methods
# ----------------------------------------------------------------------------------
@validate("_coords")
def _coords_validate(self, proposal):
coords = proposal["value"]
if not coords:
return None
for id, coord in enumerate(coords):
if coord and not isinstance(coord, Coord | CoordSet):
raise TypeError(
"At this point all passed coordinates should be of type Coord or CoordSet!",
) # coord = #
# Coord(coord)
if self._copy:
coords[id] = coord.copy()
else:
coords[id] = coord
for coord in coords:
if isinstance(coord, CoordSet):
# it must be a single dimension axis
# in this case we must have same length for all coordinates
coord._is_same_dim = True
# check this is valid in term of size
try:
_ = coord.sizes
except ValueError:
raise
# change the internal names
n = len(coord)
coord._set_names(
[f"_{i + 1}" for i in range(n)],
) # we must have _1 for the first coordinates,
# _2 the second, etc...
coord._set_parent_dim(coord.name)
# last check and sorting
names = []
for coord in coords:
if coord.has_defined_name:
names.append(coord.name)
else:
raise ValueError(
"At this point all passed coordinates should have a valid name!",
)
if coords:
if self._sorted:
_sortedtuples = sorted(
(coord.name, coord) for coord in coords
) # Final sort
coords = list(zip(*_sortedtuples, strict=False))[1]
return list(coords) # be sure its a list not a tuple
return None
@default("_id")
def _id_default(self):
# a unique id
return f"{type(self).__name__}_{str(uuid.uuid1()).split('-')[0]}"
# ----------------------------------------------------------------------------------
# Readonly Properties
# ----------------------------------------------------------------------------------
@property
def available_names(self):
"""
Chars that can be used for dimension name (list).
It returns DEFAULT_DIM_NAMES less those already in use.
"""
_available_names = DEFAULT_DIM_NAME.copy()
for item in self.names:
if item in _available_names:
_available_names.remove(item)
return _available_names
@property
def coords(self):
"""Coordinates in the coordset (list)."""
return self._coords
@property
def has_defined_name(self):
"""True if the name has been defined (bool)."""
return self.name != self.id
@property
def id(self):
"""Object identifier (Readonly property)."""
return self._id
@property
def is_empty(self):
"""True if there is no coords defined (bool)."""
if self._coords:
return len(self._coords) == 0
return False
@property
def is_same_dim(self):
"""True if the coords define a single dimension (bool)."""
return self._is_same_dim
@property
def references(self):
return self._references
@property
def sizes(self):
"""
Sizes of the coord object for each dimension (int or tuple of int).
(readonly property). If the set is for a single dimension return a
single size as all coordinates must have the same.
"""
_sizes = []
for _i, item in enumerate(self._coords):
_sizes.append(item.size) # recurrence if item is a CoordSet
if self.is_same_dim:
_sizes = list(set(_sizes))
if len(_sizes) > 1:
raise ValueError(
"Coordinates must be of the same size for a dimension with multiple "
"coordinates",
)
return _sizes[0]
return _sizes
# alias
size = sizes
# @property
# def coords(self): #TODO: replace with itertiems, items etc ... to simulate a dict
# """
# list - list of the Coord objects in the current coords (readonly
# property).
# """
# return self._coords
@property
def names(self):
"""Names of the coords in the current coords (list - read only property)."""
_names = []
if self._coords:
for item in self._coords:
if item.has_defined_name:
_names.append(item.name)
return _names
# ----------------------------------------------------------------------------------
# Mutable Properties
# ----------------------------------------------------------------------------------
@property
def default(self):
"""Default coordinates (Coord)."""
return self._coords[self._default]
@property
def data(self):
# in case data is called on a coordset for dimension with multiple coordinates
# return the default coordinates data
return self.default.data
@property
def name(self):
if self._name:
return self._name
return self._id
@name.setter
def name(self, value):
if value is not None:
self._name = value
@property
def titles(self):
"""Titles of the coords in the current coords (list)."""
_titles = []
for item in self._coords:
if isinstance(item, Coord):
_titles.append(item.title if item.title else item.name) # TODO:name
elif isinstance(item, CoordSet):
_titles.append(
[el.title if el.title else el.name for el in item._coords],
) # TODO:name
else:
raise ValueError("Something wrong with the titles!")
return _titles
@property
def labels(self):
"""Labels of the coordinates in the current coordset (list)."""
return [item.labels for item in self._coords]
@property
def is_labeled(self):
"""Returns True if one of the coords is labeled."""
return any(item.is_labeled for item in self._coords)
@property
def units(self):
"""Units of the coords in the current coords (list)."""
return [item.units for item in self._coords]
# ----------------------------------------------------------------------------------
# Public methods
# ----------------------------------------------------------------------------------
[docs]
def copy(self, keepname=False):
"""
Make a disconnected copy of the current coords.
Returns
-------
object
an exact copy of the current object
"""
return self.__copy__()
[docs]
def keys(self):
"""
Alias for names.
Returns
-------
out : list
list of all coordinates names (including reference to other coordinates).
"""
keys = []
if self.names:
keys.extend(self.names)
if self._references:
keys.extend(list(self.references.keys()))
return keys
[docs]
def select(self, val):
"""Select the default coord index."""
self._default = min(max(0, int(val) - 1), len(self.names))
[docs]
def set(self, *args, **kwargs):
"""Set one or more coordinates in the current CoordSet."""
if not args and not kwargs:
return
if len(args) == 1 and (is_sequence(args[0]) or isinstance(args[0], CoordSet)):
args = args[0]
if isinstance(args, CoordSet):
kwargs.update(args.to_dict())
args = ()
if args:
self._coords = [] # reset
for _i, item in enumerate(args[::-1]):
item.name = self.available_names.pop()
self._append(item)
for k, item in kwargs.items():
if isinstance(item, CoordSet):
# try to keep this parameter to True!
item._is_same_dim = True
self[k] = item
[docs]
def set_titles(self, *args, **kwargs):
"""
Set one or more coord title at once.
Parameters
----------
args : str(s)
The list of titles to apply to the set of coordinates (they must be given
according to the coordinate's name
alphabetical order.
**kwargs
Keyword attribution of the titles. The keys must be valid names among the
coordinate's name list. This
is the recommended way to set titles as this will be less prone to errors.
Notes
-----
If the args are not named, then the attributions are made in coordinate's name
alphabetical order :
e.g, the first title will be for the `x` coordinates, the second for the `y` ,
etc.
"""
if len(args) == 1 and (is_sequence(args[0]) or isinstance(args[0], CoordSet)):
args = args[0]
for i, item in enumerate(args):
if not isinstance(self._coords[i], CoordSet):
self._coords[i].title = item
elif is_sequence(item):
for j, v in enumerate(self._coords[i]._coords):
v.title = item[j]
for k, item in kwargs.items():
self[k].title = item
[docs]
def set_units(self, *args, **kwargs):
"""
Set one or more coord units at once.
Parameters
----------
*args : str(s)
The list of units to apply to the set of coordinates (they must be given
according to the coordinate's name
alphabetical order.
**kwargs
Keyword attribution of the units. The keys must be valid names among the
coordinate's name list. This
is the recommended way to set units as this will be less prone to errors.
force : bool, optional, default=False
Whether or not the new units must be compatible with the current units. See
the `Coord` .`to` method.
Notes
-----
If the args are not named, then the attributions are made in coordinate's name
alphabetical order :
e.g, the first units will be for the `x` coordinates, the second for the `y` , etc.
"""
force = kwargs.pop("force", False)
if len(args) == 1 and is_sequence(args[0]):
args = args[0]
for i, item in enumerate(args):
if not isinstance(self._coords[i], CoordSet):
self._coords[i].to(item, force=force, inplace=True)
elif is_sequence(item):
for j, v in enumerate(self._coords[i]._coords):
v.to(item[j], force=force, inplace=True)
for k, item in kwargs.items():
self[k].to(item, force=force, inplace=True)
[docs]
def to_dict(self):
"""
Return a dict of the coordinates from the coordset.
Returns
-------
out : dict
A dictionary where keys are the names of the coordinates, and the values
the coordinates themselves.
"""
return dict(zip(self.names, self._coords, strict=False))
[docs]
def update(self, **kwargs):
"""
Update a specific coordinates in the CoordSet.
Parameters
----------
k**warg
Only keywords among the CoordSet.names are allowed - they denotes the name
of a dimension.
"""
dims = kwargs.keys()
for dim in list(dims)[:]:
if dim in self.names:
# we can replace the given coordinates
idx = self.names.index(dim)
self[idx] = Coord(kwargs.pop(dim), name=dim)
# ----------------------------------------------------------------------------------
# private methods
# ----------------------------------------------------------------------------------
def _append(self, coord):
# utility function to append coordinate with full validation
if not isinstance(coord, tuple):
coord = (coord,)
if self._coords:
# some coordinates already present, prepend the new one
self._coords = (*coord,) + tuple(
self._coords,
) # instead of append, fire the validation process
else:
# no coordinates yet, start a new tuple of coordinate
self._coords = (*coord,)
def _loc2index(self, loc):
# Return the index of a location
for coord in self.coords:
try:
return coord._loc2index(loc)
except IndexError:
continue
# not found!
raise IndexError
def _set_names(self, names):
# utility function to change names of coordinates (in batch)
# useful when a coordinate is a CoordSet itself
for coord, name in zip(self._coords, names, strict=False):
coord.name = name
def _set_parent_dim(self, name):
# utility function to set the paretn name for sub coordset
for coord in self._coords:
coord._parent_dim = name
# ----------------------------------------------------------------------------------
# special methods
# ----------------------------------------------------------------------------------
# @staticmethod
def __dir__(self):
return ["coords", "references", "is_same_dim", "name"]
def __call__(self, *args, **kwargs):
# allow the following syntax: coords(), coords(0,2) or
coords = []
axis = kwargs.get("axis")
if args:
for idx in args:
coords.append(self[idx])
elif axis is not None:
if not is_sequence(axis):
axis = [axis]
for i in axis:
coords.append(self[i])
else:
coords = self._coords
if len(coords) == 1:
return coords[0]
return CoordSet(*coords)
def __hash__(self):
# all instance of this class has same hash, so they can be compared
return hash(tuple(self._coords))
def __len__(self):
return len(self._coords)
def __delattr__(self, item):
if "notify_change" in item:
return None
try:
return self.__delitem__(item)
except (IndexError, KeyError):
raise AttributeError from None
def __getattr__(self, item):
# when the attribute was not found
if "_validate" in item or "_changed" in item or item in ["strip", "__iter__"]:
raise AttributeError
try:
return self.__getitem__(item)
except (IndexError, KeyError):
raise AttributeError from None
def __getitem__(self, index):
# String index
# ------------
if isinstance(index, str):
# find by name
if index in self.names:
idx = self.names.index(index)
return self._coords.__getitem__(idx)
# ok we did not find it!
# let's try in references
if index in self._references:
return self._references[index]
# let's try in the title
if index in self.titles:
# selection by coord titles
if self.titles.count(index) > 1:
warnings.warn(
f"Getting a coordinate from its title. However `{index}` occurs "
f"several time. Only"
f" the first occurrence is returned!",
stacklevel=2,
)
return self._coords.__getitem__(self.titles.index(index))
# may be it is a title or a name in a sub-coords
for item in self._coords:
if isinstance(item, CoordSet) and index in item.titles:
# selection by subcoord title
return item._coords.__getitem__(item.titles.index(index))
for item in self._coords:
if isinstance(item, CoordSet) and index in item.names:
# selection by subcoord name
return item._coords.__getitem__(item.names.index(index))
try:
# let try with the canonical dimension names
if index[0] in self.names:
# ok we can find it a a canonical name:
c = self._coords.__getitem__(self.names.index(index[0]))
if len(index) > 1 and index[1] == "_":
if isinstance(c, CoordSet):
c = c.__getitem__(index[1:])
else:
c = c.__getitem__(index[2:]) # try on labels
return c
except IndexError:
pass
raise KeyError(f"Could not find `{index}` in coordinates names or titles")
# numerical index
# ---------------
# It can be that we are dealing with slicing of multicoordinates
multi = bool(self.is_same_dim)
if not multi:
return self._coords.__getitem__(index) # It's the index of one of the
# coordinate in the coordset. return it.
res = [] # Slice of a multicoordinate
for c in self._coords:
res.append(c.__getitem__(index))
coords = self.__class__(*res, keepnames=True)
# name must be changed
coords.name = self.name
# and is_same_dim and default for coordset
coords._is_same_dim = self._is_same_dim
coords._default = self._default
return coords
# if isinstance(index, slice):
# if isinstance(res, CoordSet):
# res = (res,)
# return CoordSet(*res, keepnames=True)
# else:
# return res
def __setattr__(self, key, value):
keyb = key[1:] if key.startswith("_") else key
if keyb in [
"parent",
"copy",
"sorted",
"coords",
"updated",
"name",
"html_output",
"is_same_dim",
"parent_dim",
"trait_values",
"trait_notifiers",
"trait_validators",
"cross_validation_lock",
"notify_change",
]:
super().__setattr__(key, value)
return
try:
self.__setitem__(key, value)
except Exception:
super().__setattr__(key, value)
def __setitem__(self, index, coord):
try:
coord = coord.copy(keepname=True) # to avoid modifying the original
except TypeError as e:
if isinstance(coord, list):
coord = [c.copy(keepname=True) for c in coord[:]]
else:
raise e
if isinstance(index, str):
# find by name
if index in self.names:
idx = self.names.index(index)
coord.name = index
self._coords.__setitem__(idx, coord)
return
# ok we did not find it!
# let's try in the title
if index in self.titles:
# selection by coord titles
if self.titles.count(index) > 1:
warnings.warn(
f"Getting a coordinate from its title. However `{index}` "
f"occurs several time. Only"
f" the first occurrence is returned!",
stacklevel=2,
)
index = self.titles.index(index)
coord.name = self.names[index]
self._coords.__setitem__(index, coord)
return
# may be it is a title or a name in a sub-coords
for item in self._coords:
if isinstance(item, CoordSet) and index in item.titles:
# selection by subcoord title
index = item.titles.index(index)
coord.name = item.names[index]
item.__setitem__(index, coord)
return
for item in self._coords:
if isinstance(item, CoordSet) and index in item.names:
# selection by subcoord title
index = item.names.index(index)
coord.name = item.names[index]
item.__setitem__(index, coord)
return
try:
# let try with the canonical dimension names
if index[0] in self.names:
# ok we can find it a a canonical name:
c = self._coords.__getitem__(self.names.index(index[0]))
if len(index) > 1 and index[1] == "_":
c.__setitem__(index[1:], coord)
return
except KeyError:
pass
# add the new coordinates
if index in self.available_names or (
len(index) == 2
and index.startswith("_")
and index[1] in list("123456789")
):
coord.name = index
self._coords.append(coord)
return
raise KeyError(f"Could not find `{index}` in coordinates names or titles")
self._coords[index] = coord
def __delitem__(self, index):
if isinstance(index, str):
# find by name
if index in self.names:
idx = self.names.index(index)
del self._coords[idx]
return None
# let's try in the title
if index in self.titles:
# selection by coord titles
index = self.titles.index(index)
self._coords.__delitem__(index)
return None
# may be it is a title in a sub-coords
for item in self._coords:
if isinstance(item, CoordSet) and index in item.titles:
# selection by subcoord title
return item.__delitem__(index)
# let try with the canonical dimension names
if index[0] in self.names:
# ok we can find it a a canonical name:
c = self._coords.__getitem__(self.names.index(index[0]))
if len(index) > 1 and index[1] == "_" and isinstance(c, CoordSet):
return c.__delitem__(index[1:])
raise KeyError(f"Could not find `{index}` in coordinates names or titles")
return None
# def __iter__(self):
# for item in self._coords:
# yield item
def __repr__(self):
out = "CoordSet: [" + ", ".join(["{}"] * len(self._coords)) + "]"
s = []
for item in self._coords:
if isinstance(item, CoordSet):
s.append(f"{item.name}:" + repr(item).replace("CoordSet: ", ""))
else:
s.append(f"{item.name}:{item.title}")
return out.format(*s)
def __str__(self):
return repr(self)
def _cstr(self, header=" coordinates: ... \n", print_size=True):
txt = ""
for _idx, dim in enumerate(self.names):
coord = getattr(self, dim)
if coord:
dimension = f" DIMENSION `{dim}`"
for k, v in self.references.items():
if dim == v:
# reference to this dimension
dimension += f"=`{k}`"
txt += dimension + "\n"
if isinstance(coord, CoordSet):
# txt += ' index: {}\n'.format(idx)
if not coord.is_empty:
if print_size:
txt += f"{coord._coords[0]._str_shape().rstrip()}\n"
coord._html_output = self._html_output
for _idx_s, dim_s in enumerate(coord.names):
c = getattr(coord, dim_s)
txt += f" ({dim_s}) ...\n"
c._html_output = self._html_output
sub = c._cstr(
header=" coordinates: ... \n",
print_size=False,
) # , indent=4, first_indent=-6)
txt += f"{sub}\n"
elif not coord.is_empty:
# coordinates if available
# txt += ' index: {}\n'.format(idx)
coord._html_output = self._html_output
txt += f"{coord._cstr(header=header, print_size=print_size)}\n"
txt = txt.rstrip() # remove the trailing '\n'
if not self._html_output:
return colored_output(txt.rstrip())
return txt.rstrip()
def _repr_html_(self):
return convert_to_html(self)
def __deepcopy__(self, memo):
coords = self.__class__(
tuple(cpy.deepcopy(ax, memo=memo) for ax in self._coords),
keepnames=True,
)
coords.name = self.name
coords._is_same_dim = self._is_same_dim
coords._default = self._default
return coords
def __copy__(self):
coords = self.__class__(
tuple(cpy.copy(ax) for ax in self._coords),
keepnames=True,
)
# name must be changed
coords.name = self.name
# and is_same_dim and default for coordset
coords._is_same_dim = self._is_same_dim
coords._default = self._default
return coords
def __eq__(self, other):
if other is None:
return False
try:
return self._coords == other._coords
except Exception:
return False
def __ne__(self, other):
return not self.__eq__(other)
# ----------------------------------------------------------------------------------
# Events
# ----------------------------------------------------------------------------------
def _coords_update(self, change):
# when notified that a coord name have been updated
self._updated = True
@observe(All)
def _anytrait_changed(self, change):
# ex: change {
# 'owner': object, # The HasTraits instance
# 'new': 6, # The new value
# 'old': 5, # The old value
# 'name': "foo", # The name of the changed trait
# 'type': 'change', # The event type of the notification, usually 'change'
# }
if change.name == "_updated" and change.new:
self._updated = False # reset