Coverage for python/lsst/daf/butler/dimensions/_data_coordinate_iterable.py: 36%
228 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:24 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:24 -0700
1# This file is part of daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30__all__ = (
31 "DataCoordinateIterable",
32 "DataCoordinateSet",
33 "DataCoordinateSequence",
34)
36import warnings
37from abc import abstractmethod
38from collections.abc import Collection, Iterable, Iterator, Sequence, Set
39from typing import Any, overload
41from deprecated.sphinx import deprecated
42from lsst.utils.introspection import find_outside_stacklevel
44from ._coordinate import DataCoordinate
45from ._graph import DimensionGraph
46from ._group import DimensionGroup
47from ._universe import DimensionUniverse
50class DataCoordinateIterable(Iterable[DataCoordinate]):
51 """An abstract base class for homogeneous iterables of data IDs.
53 All elements of a `DataCoordinateIterable` identify the same set of
54 dimensions (given by the `graph` property) and generally have the same
55 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values.
56 """
58 __slots__ = ()
60 @staticmethod
61 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable:
62 """Return a `DataCoordinateIterable` containing the single data ID.
64 Parameters
65 ----------
66 dataId : `DataCoordinate`
67 Data ID to adapt. Must be a true `DataCoordinate` instance, not
68 an arbitrary mapping. No runtime checking is performed.
70 Returns
71 -------
72 iterable : `DataCoordinateIterable`
73 A `DataCoordinateIterable` instance of unspecified (i.e.
74 implementation-detail) subclass. Guaranteed to implement
75 the `collections.abc.Sized` (i.e. `__len__`) and
76 `collections.abc.Container` (i.e. `__contains__`) interfaces as
77 well as that of `DataCoordinateIterable`.
78 """
79 return _ScalarDataCoordinateIterable(dataId)
81 # TODO: remove on DM-41326.
82 @property
83 @deprecated(
84 "Deprecated in favor of .dimensions; will be removed after v26.",
85 category=FutureWarning,
86 version="v27",
87 )
88 def graph(self) -> DimensionGraph:
89 """Dimensions identified by these data IDs (`DimensionGraph`)."""
90 return self.dimensions._as_graph()
92 @property
93 @abstractmethod
94 def dimensions(self) -> DimensionGroup:
95 """Dimensions identified by these data IDs (`DimensionGroup`)."""
96 raise NotImplementedError()
98 @property
99 def universe(self) -> DimensionUniverse:
100 """Universe that defines all known compatible dimensions.
102 (`DimensionUniverse`).
103 """
104 return self.dimensions.universe
106 @abstractmethod
107 def hasFull(self) -> bool:
108 """Indicate if all data IDs in this iterable identify all dimensions.
110 Not just required dimensions.
112 Returns
113 -------
114 state : `bool`
115 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed.
116 If `False`, no guarantees are made.
117 """
118 raise NotImplementedError()
120 @abstractmethod
121 def hasRecords(self) -> bool:
122 """Return whether all data IDs in this iterable contain records.
124 Returns
125 -------
126 state : `bool`
127 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed.
128 If `False`, no guarantees are made.
129 """
130 raise NotImplementedError()
132 def toSet(self) -> DataCoordinateSet:
133 """Transform this iterable into a `DataCoordinateSet`.
135 Returns
136 -------
137 set : `DataCoordinateSet`
138 A `DatasetCoordinateSet` instance with the same elements as
139 ``self``, after removing any duplicates. May be ``self`` if it is
140 already a `DataCoordinateSet`.
141 """
142 return DataCoordinateSet(
143 frozenset(self),
144 dimensions=self.dimensions,
145 hasFull=self.hasFull(),
146 hasRecords=self.hasRecords(),
147 check=False,
148 )
150 def toSequence(self) -> DataCoordinateSequence:
151 """Transform this iterable into a `DataCoordinateSequence`.
153 Returns
154 -------
155 seq : `DataCoordinateSequence`
156 A new `DatasetCoordinateSequence` with the same elements as
157 ``self``, in the same order. May be ``self`` if it is already a
158 `DataCoordinateSequence`.
159 """
160 return DataCoordinateSequence(
161 tuple(self),
162 dimensions=self.dimensions,
163 hasFull=self.hasFull(),
164 hasRecords=self.hasRecords(),
165 check=False,
166 )
168 @abstractmethod
169 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateIterable:
170 """Return a subset iterable.
172 This subset iterable returns data IDs that identify a subset of the
173 dimensions that this one's do.
175 Parameters
176 ----------
177 dimensions : `DimensionGraph`, `DimensionGroup`, or \
178 `~collections.abc.Iterable` [ `str` ]
179 Dimensions to be identified by the data IDs in the returned
180 iterable. Must be a subset of ``self.dimensions``.
182 Returns
183 -------
184 iterable : `DataCoordinateIterable`
185 A `DataCoordinateIterable` with
186 ``iterable.dimensions == dimensions``.
187 May be ``self`` if ``dimensions == self.dimensions``. Elements are
188 equivalent to those that would be created by calling
189 `DataCoordinate.subset` on all elements in ``self``, possibly
190 with deduplication and/or reordering (depending on the subclass,
191 which may make more specific guarantees).
192 """
193 raise NotImplementedError()
196class _ScalarDataCoordinateIterable(DataCoordinateIterable):
197 """An iterable for a single `DataCoordinate`.
199 A `DataCoordinateIterable` implementation that adapts a single
200 `DataCoordinate` instance.
202 This class should only be used directly by other code in the module in
203 which it is defined; all other code should interact with it only through
204 the `DataCoordinateIterable` interface.
206 Parameters
207 ----------
208 dataId : `DataCoordinate`
209 The data ID to adapt.
210 """
212 def __init__(self, dataId: DataCoordinate):
213 self._dataId = dataId
215 __slots__ = ("_dataId",)
217 def __iter__(self) -> Iterator[DataCoordinate]:
218 yield self._dataId
220 def __len__(self) -> int:
221 return 1
223 def __contains__(self, key: Any) -> bool:
224 if isinstance(key, DataCoordinate):
225 return key == self._dataId
226 else:
227 return False
229 @property
230 def dimensions(self) -> DimensionGroup:
231 # Docstring inherited from DataCoordinateIterable.
232 return self._dataId.dimensions
234 def hasFull(self) -> bool:
235 # Docstring inherited from DataCoordinateIterable.
236 return self._dataId.hasFull()
238 def hasRecords(self) -> bool:
239 # Docstring inherited from DataCoordinateIterable.
240 return self._dataId.hasRecords()
242 def subset(
243 self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]
244 ) -> _ScalarDataCoordinateIterable:
245 # Docstring inherited from DataCoordinateIterable.
246 dimensions = self.universe.conform(dimensions)
247 return _ScalarDataCoordinateIterable(self._dataId.subset(dimensions))
250class _DataCoordinateCollectionBase(DataCoordinateIterable):
251 """A partial iterable implementation backed by native Python collection.
253 A partial `DataCoordinateIterable` implementation that is backed by a
254 native Python collection.
256 This class is intended only to be used as an intermediate base class for
257 `DataCoordinateIterables` that assume a more specific type of collection
258 and can hence make more informed choices for how to implement some methods.
260 Parameters
261 ----------
262 dataIds : `collections.abc.Collection` [ `DataCoordinate` ]
263 A collection of `DataCoordinate` instances, with dimensions equal to
264 ``dimensions``.
265 graph : `DimensionGraph`, optional
266 Dimensions identified by all data IDs in the collection. Ignored if
267 ``dimensions`` is provided, and deprecated with removal after v27.
268 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`, \
269 or `DimensionGraph`, optional
270 Dimensions identified by all data IDs in the collection. Must be
271 provided unless ``graph`` is.
272 hasFull : `bool`, optional
273 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
274 `True` for all given data IDs. If `False`, no such guarantee is made,
275 and `hasFull` will always return `False`. If `None` (default),
276 `hasFull` will be computed from the given data IDs, immediately if
277 ``check`` is `True`, or on first use if ``check`` is `False`.
278 hasRecords : `bool`, optional
279 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
280 returns `True` for all given data IDs. If `False`, no such guarantee
281 is made and `hasRecords` will always return `False`. If `None`
282 (default), `hasRecords` will be computed from the given data IDs,
283 immediately if ``check`` is `True`, or on first use if ``check`` is
284 `False`.
285 check: `bool`, optional
286 If `True` (default) check that all data IDs are consistent with the
287 given ``graph`` and state flags at construction. If `False`, no
288 checking will occur.
289 universe : `DimensionUniverse`
290 Object that manages all dimension definitions.
291 """
293 def __init__(
294 self,
295 dataIds: Collection[DataCoordinate],
296 graph: DimensionGraph | None = None,
297 *,
298 dimensions: Iterable[str] | DimensionGroup | DimensionGraph | None = None,
299 hasFull: bool | None = None,
300 hasRecords: bool | None = None,
301 check: bool = True,
302 universe: DimensionUniverse | None = None,
303 ):
304 universe = (
305 universe
306 or getattr(dimensions, "universe", None)
307 or getattr(graph, "universe", None)
308 or getattr(dataIds, "universe", None)
309 )
310 if universe is None:
311 raise TypeError(
312 "universe must be provided, either directly or via dimensions, dataIds, or graph."
313 )
314 if graph is not None:
315 warnings.warn(
316 "The 'graph' argument to DataCoordinateIterable constructors is deprecated in favor of "
317 " passing an iterable of dimension names as the 'dimensions' argument, and wil be removed "
318 "after v27.",
319 stacklevel=find_outside_stacklevel("lsst.daf.butler"),
320 category=FutureWarning,
321 )
322 if dimensions is not None:
323 dimensions = universe.conform(dimensions)
324 elif graph is not None:
325 dimensions = graph.as_group()
326 del graph # Avoid accidental use later.
327 if dimensions is None:
328 raise TypeError("Exactly one of 'graph' or (preferably) 'dimensions' must be provided.")
329 self._dataIds = dataIds
330 self._dimensions = dimensions
331 if check:
332 for dataId in self._dataIds:
333 if hasFull and not dataId.hasFull():
334 raise ValueError(f"{dataId} is not complete, but is required to be.")
335 if hasRecords and not dataId.hasRecords():
336 raise ValueError(f"{dataId} has no records, but is required to.")
337 if dataId.dimensions != self._dimensions:
338 raise ValueError(f"Bad dimensions {dataId.dimensions}; expected {self._dimensions}.")
339 if hasFull is None:
340 hasFull = all(dataId.hasFull() for dataId in self._dataIds)
341 if hasRecords is None:
342 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
343 self._hasFull = hasFull
344 self._hasRecords = hasRecords
346 __slots__ = ("_dimensions", "_dataIds", "_hasFull", "_hasRecords")
348 @property
349 def dimensions(self) -> DimensionGroup:
350 # Docstring inherited from DataCoordinateIterable.
351 return self._dimensions
353 def hasFull(self) -> bool:
354 # Docstring inherited from DataCoordinateIterable.
355 if self._hasFull is None:
356 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds)
357 return self._hasFull
359 def hasRecords(self) -> bool:
360 # Docstring inherited from DataCoordinateIterable.
361 if self._hasRecords is None:
362 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
363 return self._hasRecords
365 def toSet(self) -> DataCoordinateSet:
366 # Docstring inherited from DataCoordinateIterable.
367 # Override base class to pass in attributes instead of results of
368 # method calls for _hasFull and _hasRecords - those can be None,
369 # and hence defer checking if that's what the user originally wanted.
370 return DataCoordinateSet(
371 frozenset(self._dataIds),
372 dimensions=self._dimensions,
373 hasFull=self._hasFull,
374 hasRecords=self._hasRecords,
375 check=False,
376 )
378 def toSequence(self) -> DataCoordinateSequence:
379 # Docstring inherited from DataCoordinateIterable.
380 # Override base class to pass in attributes instead of results of
381 # method calls for _hasFull and _hasRecords - those can be None,
382 # and hence defer checking if that's what the user originally wanted.
383 return DataCoordinateSequence(
384 tuple(self._dataIds),
385 dimensions=self._dimensions,
386 hasFull=self._hasFull,
387 hasRecords=self._hasRecords,
388 check=False,
389 )
391 def __iter__(self) -> Iterator[DataCoordinate]:
392 return iter(self._dataIds)
394 def __len__(self) -> int:
395 return len(self._dataIds)
397 def __contains__(self, key: Any) -> bool:
398 key = DataCoordinate.standardize(key, universe=self.universe)
399 return key in self._dataIds
401 def _subsetKwargs(self, dimensions: DimensionGroup) -> dict[str, Any]:
402 """Return constructor kwargs useful for subclasses implementing subset.
404 Parameters
405 ----------
406 dimensions : `DimensionGroup`
407 Dimensions passed to `subset`.
409 Returns
410 -------
411 **kwargs
412 A dict with `hasFull`, `hasRecords`, and `check` keys, associated
413 with the appropriate values for a `subset` operation with the given
414 dimensions.
415 """
416 hasFull: bool | None
417 if dimensions.names <= self.dimensions.required:
418 hasFull = True
419 else:
420 hasFull = self._hasFull
421 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False)
424class DataCoordinateSet(_DataCoordinateCollectionBase):
425 """Iterable iteration that is set-like.
427 A `DataCoordinateIterable` implementation that adds some set-like
428 functionality, and is backed by a true set-like object.
430 Parameters
431 ----------
432 dataIds : `collections.abc.Set` [ `DataCoordinate` ]
433 A set of `DataCoordinate` instances, with dimensions equal to
434 ``graph``. If this is a mutable object, the caller must be able to
435 guarantee that it will not be modified by any other holders.
436 graph : `DimensionGraph`, optional
437 Dimensions identified by all data IDs in the collection. Ignored if
438 ``dimensions`` is provided, and deprecated with removal after v27.
439 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`, \
440 or `DimensionGraph`, optional
441 Dimensions identified by all data IDs in the collection. Must be
442 provided unless ``graph`` is.
443 hasFull : `bool`, optional
444 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
445 `True` for all given data IDs. If `False`, no such guarantee is made,
446 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
447 (default), `DataCoordinateSet.hasFull` will be computed from the given
448 data IDs, immediately if ``check`` is `True`, or on first use if
449 ``check`` is `False`.
450 hasRecords : `bool`, optional
451 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
452 returns `True` for all given data IDs. If `False`, no such guarantee
453 is made and `DataCoordinateSet.hasRecords` will always return `False`.
454 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
455 from the given data IDs, immediately if ``check`` is `True`, or on
456 first use if ``check`` is `False`.
457 check : `bool`, optional
458 If `True` (default) check that all data IDs are consistent with the
459 given ``graph`` and state flags at construction. If `False`, no
460 checking will occur.
461 universe : `DimensionUniverse`
462 Object that manages all dimension definitions.
464 Notes
465 -----
466 `DataCoordinateSet` does not formally implement the `collections.abc.Set`
467 interface, because that requires many binary operations to accept any
468 set-like object as the other argument (regardless of what its elements
469 might be), and it's much easier to ensure those operations never behave
470 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes)
471 `DataCoordinateIterable`, and in most cases restrict that they identify
472 the same dimensions. In particular:
474 - a `DataCoordinateSet` will compare as not equal to any object that is
475 not a `DataCoordinateSet`, even native Python sets containing the exact
476 same elements;
478 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``)
479 require both operands to be `DataCoordinateSet` instances that have the
480 same dimensions (i.e. `dimensions` attribute);
482 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to
483 be a `DataCoordinateIterable` with the same dimensions;
485 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both
486 operands to be `DataCoordinateSet` instances that have the same
487 dimensions _and_ the same ``dtype``;
489 - named methods that create new sets (`intersection`, `union`,
490 `symmetric_difference`, `difference`) require the other operand to be a
491 `DataCoordinateIterable` with the same dimensions _and_ the same
492 ``dtype``.
494 In addition, when the two operands differ in the return values of `hasFull`
495 and/or `hasRecords`, we make no guarantees about what those methods will
496 return on the new `DataCoordinateSet` (other than that they will accurately
497 reflect what elements are in the new set - we just don't control which
498 elements are contributed by each operand).
499 """
501 def __init__(
502 self,
503 dataIds: Set[DataCoordinate],
504 graph: DimensionGraph | None = None,
505 *,
506 dimensions: Iterable[str] | DimensionGroup | DimensionGraph | None = None,
507 hasFull: bool | None = None,
508 hasRecords: bool | None = None,
509 check: bool = True,
510 universe: DimensionUniverse | None = None,
511 ):
512 super().__init__(
513 dataIds,
514 graph,
515 dimensions=dimensions,
516 hasFull=hasFull,
517 hasRecords=hasRecords,
518 check=check,
519 universe=universe,
520 )
522 _dataIds: Set[DataCoordinate]
524 __slots__ = ()
526 def __str__(self) -> str:
527 return str(set(self._dataIds))
529 def __repr__(self) -> str:
530 return (
531 f"DataCoordinateSet({set(self._dataIds)}, {self._dimensions!r}, "
532 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})"
533 )
535 def __eq__(self, other: Any) -> bool:
536 if isinstance(other, DataCoordinateSet):
537 return self._dimensions == other._dimensions and self._dataIds == other._dataIds
538 return False
540 def __le__(self, other: DataCoordinateSet) -> bool:
541 if self.dimensions != other.dimensions:
542 raise ValueError(
543 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
544 )
545 return self._dataIds <= other._dataIds
547 def __ge__(self, other: DataCoordinateSet) -> bool:
548 if self.dimensions != other.dimensions:
549 raise ValueError(
550 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
551 )
552 return self._dataIds >= other._dataIds
554 def __lt__(self, other: DataCoordinateSet) -> bool:
555 if self.dimensions != other.dimensions:
556 raise ValueError(
557 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
558 )
559 return self._dataIds < other._dataIds
561 def __gt__(self, other: DataCoordinateSet) -> bool:
562 if self.dimensions != other.dimensions:
563 raise ValueError(
564 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
565 )
566 return self._dataIds > other._dataIds
568 def issubset(self, other: DataCoordinateIterable) -> bool:
569 """Test whether ``self`` contains all data IDs in ``other``.
571 Parameters
572 ----------
573 other : `DataCoordinateIterable`
574 An iterable of data IDs with ``other.graph == self.graph``.
576 Returns
577 -------
578 issubset : `bool`
579 `True` if all data IDs in ``self`` are also in ``other``, and
580 `False` otherwise.
581 """
582 if self.dimensions != other.dimensions:
583 raise ValueError(
584 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
585 )
586 return self._dataIds <= other.toSet()._dataIds
588 def issuperset(self, other: DataCoordinateIterable) -> bool:
589 """Test whether ``other`` contains all data IDs in ``self``.
591 Parameters
592 ----------
593 other : `DataCoordinateIterable`
594 An iterable of data IDs with
595 ``other.dimensions == self.dimensions``.
597 Returns
598 -------
599 issuperset : `bool`
600 `True` if all data IDs in ``other`` are also in ``self``, and
601 `False` otherwise.
602 """
603 if self.dimensions != other.dimensions:
604 raise ValueError(
605 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
606 )
607 return self._dataIds >= other.toSet()._dataIds
609 def isdisjoint(self, other: DataCoordinateIterable) -> bool:
610 """Test whether there are no data IDs in both ``self`` and ``other``.
612 Parameters
613 ----------
614 other : `DataCoordinateIterable`
615 An iterable of data IDs with
616 ``other._dimensions == self._dimensions``.
618 Returns
619 -------
620 isdisjoint : `bool`
621 `True` if there are no data IDs in both ``self`` and ``other``, and
622 `False` otherwise.
623 """
624 if self._dimensions != other.dimensions:
625 raise ValueError(
626 f"Inconsistent dimensions in set comparision: {self._dimensions} != {other.dimensions}."
627 )
628 return self._dataIds.isdisjoint(other.toSet()._dataIds)
630 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet:
631 if self._dimensions != other.dimensions:
632 raise ValueError(
633 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
634 )
635 return DataCoordinateSet(self._dataIds & other._dataIds, dimensions=self._dimensions, check=False)
637 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet:
638 if self._dimensions != other.dimensions:
639 raise ValueError(
640 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
641 )
642 return DataCoordinateSet(self._dataIds | other._dataIds, dimensions=self._dimensions, check=False)
644 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet:
645 if self._dimensions != other.dimensions:
646 raise ValueError(
647 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
648 )
649 return DataCoordinateSet(self._dataIds ^ other._dataIds, dimensions=self._dimensions, check=False)
651 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet:
652 if self._dimensions != other.dimensions:
653 raise ValueError(
654 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
655 )
656 return DataCoordinateSet(self._dataIds - other._dataIds, dimensions=self._dimensions, check=False)
658 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet:
659 """Return a new set that contains all data IDs from parameters.
661 Parameters
662 ----------
663 other : `DataCoordinateIterable`
664 An iterable of data IDs with
665 ``other.dimensions == self.dimensions``.
667 Returns
668 -------
669 intersection : `DataCoordinateSet`
670 A new `DataCoordinateSet` instance.
671 """
672 if self.dimensions != other.dimensions:
673 raise ValueError(
674 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
675 )
676 return DataCoordinateSet(
677 self._dataIds & other.toSet()._dataIds, dimensions=self.dimensions, check=False
678 )
680 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet:
681 """Return a new set that contains all data IDs in either parameters.
683 Parameters
684 ----------
685 other : `DataCoordinateIterable`
686 An iterable of data IDs with
687 ``other.dimensions == self.dimensions``.
689 Returns
690 -------
691 intersection : `DataCoordinateSet`
692 A new `DataCoordinateSet` instance.
693 """
694 if self.dimensions != other.dimensions:
695 raise ValueError(
696 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
697 )
698 return DataCoordinateSet(
699 self._dataIds | other.toSet()._dataIds, dimensions=self.dimensions, check=False
700 )
702 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
703 """Return a new set with all data IDs in either parameters, not both.
705 Parameters
706 ----------
707 other : `DataCoordinateIterable`
708 An iterable of data IDs with
709 ``other.dimensions == self.dimensions``.
711 Returns
712 -------
713 intersection : `DataCoordinateSet`
714 A new `DataCoordinateSet` instance.
715 """
716 if self.dimensions != other.dimensions:
717 raise ValueError(
718 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
719 )
720 return DataCoordinateSet(
721 self._dataIds ^ other.toSet()._dataIds, dimensions=self.dimensions, check=False
722 )
724 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
725 """Return a new set with all data IDs in this that are not in other.
727 Parameters
728 ----------
729 other : `DataCoordinateIterable`
730 An iterable of data IDs with
731 ``other.dimensions == self.dimensions``.
733 Returns
734 -------
735 intersection : `DataCoordinateSet`
736 A new `DataCoordinateSet` instance.
737 """
738 if self.dimensions != other.dimensions:
739 raise ValueError(
740 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
741 )
742 return DataCoordinateSet(
743 self._dataIds - other.toSet()._dataIds, dimensions=self.dimensions, check=False
744 )
746 def toSet(self) -> DataCoordinateSet:
747 # Docstring inherited from DataCoordinateIterable.
748 return self
750 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateSet:
751 """Return a set whose data IDs identify a subset.
753 Parameters
754 ----------
755 dimensions : `DimensionGraph`, `DimensionGroup`, or \
756 `~collections.abc.Iterable` [ `str` ]
757 Dimensions to be identified by the data IDs in the returned
758 iterable. Must be a subset of ``self.dimensions``.
760 Returns
761 -------
762 set : `DataCoordinateSet`
763 A `DataCoordinateSet` with ``set.dimensions == dimensions``. Will
764 be ``self`` if ``dimensions == self.dimensions``. Elements are
765 equivalent to those that would be created by calling
766 `DataCoordinate.subset` on all elements in ``self``, with
767 deduplication and in arbitrary order.
768 """
769 dimensions = self.universe.conform(dimensions)
770 if dimensions == self.dimensions:
771 return self
772 return DataCoordinateSet(
773 {dataId.subset(dimensions) for dataId in self._dataIds},
774 dimensions=dimensions,
775 **self._subsetKwargs(dimensions),
776 )
779class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]):
780 """Iterable supporting the full Sequence interface.
782 A `DataCoordinateIterable` implementation that supports the full
783 `collections.abc.Sequence` interface.
785 Parameters
786 ----------
787 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ]
788 A sequence of `DataCoordinate` instances, with dimensions equal to
789 ``graph``.
790 graph : `DimensionGraph`, optional
791 Dimensions identified by all data IDs in the collection. Ignored if
792 ``dimensions`` is provided, and deprecated with removal after v27.
793 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`, \
794 `DimensionGraph`, optional
795 Dimensions identified by all data IDs in the collection. Must be
796 provided unless ``graph`` is.
797 hasFull : `bool`, optional
798 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
799 `True` for all given data IDs. If `False`, no such guarantee is made,
800 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
801 (default), `DataCoordinateSet.hasFull` will be computed from the given
802 data IDs, immediately if ``check`` is `True`, or on first use if
803 ``check`` is `False`.
804 hasRecords : `bool`, optional
805 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
806 returns `True` for all given data IDs. If `False`, no such guarantee
807 is made and `DataCoordinateSet.hasRecords` will always return `False`.
808 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
809 from the given data IDs, immediately if ``check`` is `True`, or on
810 first use if ``check`` is `False`.
811 check : `bool`, optional
812 If `True` (default) check that all data IDs are consistent with the
813 given ``graph`` and state flags at construction. If `False`, no
814 checking will occur.
815 universe : `DimensionUniverse`
816 Object that manages all dimension definitions.
817 """
819 def __init__(
820 self,
821 dataIds: Sequence[DataCoordinate],
822 graph: DimensionGraph | None = None,
823 *,
824 dimensions: Iterable[str] | DimensionGroup | DimensionGraph | None = None,
825 hasFull: bool | None = None,
826 hasRecords: bool | None = None,
827 check: bool = True,
828 universe: DimensionUniverse | None = None,
829 ):
830 super().__init__(
831 tuple(dataIds),
832 graph,
833 dimensions=dimensions,
834 hasFull=hasFull,
835 hasRecords=hasRecords,
836 check=check,
837 universe=universe,
838 )
840 _dataIds: Sequence[DataCoordinate]
842 __slots__ = ()
844 def __str__(self) -> str:
845 return str(tuple(self._dataIds))
847 def __repr__(self) -> str:
848 return (
849 f"DataCoordinateSequence({tuple(self._dataIds)}, {self._dimensions!r}, "
850 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})"
851 )
853 def __eq__(self, other: Any) -> bool:
854 if isinstance(other, DataCoordinateSequence):
855 return self._dimensions == other._dimensions and self._dataIds == other._dataIds
856 return False
858 @overload
859 def __getitem__(self, index: int) -> DataCoordinate:
860 pass
862 @overload
863 def __getitem__(self, index: slice) -> DataCoordinateSequence:
864 pass
866 def __getitem__(self, index: Any) -> Any:
867 r = self._dataIds[index]
868 if isinstance(index, slice):
869 return DataCoordinateSequence(
870 r,
871 dimensions=self._dimensions,
872 hasFull=self._hasFull,
873 hasRecords=self._hasRecords,
874 check=False,
875 )
876 return r
878 def toSequence(self) -> DataCoordinateSequence:
879 # Docstring inherited from DataCoordinateIterable.
880 return self
882 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateSequence:
883 """Return a sequence whose data IDs identify a subset.
885 Parameters
886 ----------
887 dimensions : `DimensionGraph`, `DimensionGroup`, \
888 or `~collections.abc.Iterable` [ `str` ]
889 Dimensions to be identified by the data IDs in the returned
890 iterable. Must be a subset of ``self.dimensions``.
892 Returns
893 -------
894 set : `DataCoordinateSequence`
895 A `DataCoordinateSequence` with ``set.graph == graph``.
896 Will be ``self`` if ``graph == self.graph``. Elements are
897 equivalent to those that would be created by calling
898 `DataCoordinate.subset` on all elements in ``self``, in the same
899 order and with no deduplication.
900 """
901 dimensions = self.universe.conform(dimensions)
902 if dimensions == self.dimensions:
903 return self
904 return DataCoordinateSequence(
905 tuple(dataId.subset(dimensions) for dataId in self._dataIds),
906 dimensions=dimensions,
907 **self._subsetKwargs(dimensions),
908 )