Coverage for python / lsst / daf / butler / dimensions / _data_coordinate_iterable.py: 31%
214 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:37 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:37 +0000
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 "DataCoordinateSequence",
33 "DataCoordinateSet",
34)
36from abc import abstractmethod
37from collections.abc import Collection, Iterable, Iterator, Sequence, Set
38from typing import Any, overload
40from ._coordinate import DataCoordinate
41from ._group import DimensionGroup
42from ._universe import DimensionUniverse
45class DataCoordinateIterable(Iterable[DataCoordinate]):
46 """An abstract base class for homogeneous iterables of data IDs.
48 All elements of a `DataCoordinateIterable` identify the same set of
49 dimensions (given by the `dimensions` property) and generally have the same
50 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values.
51 """
53 __slots__ = ()
55 @staticmethod
56 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable:
57 """Return a `DataCoordinateIterable` containing the single data ID.
59 Parameters
60 ----------
61 dataId : `DataCoordinate`
62 Data ID to adapt. Must be a true `DataCoordinate` instance, not
63 an arbitrary mapping. No runtime checking is performed.
65 Returns
66 -------
67 iterable : `DataCoordinateIterable`
68 A `DataCoordinateIterable` instance of unspecified (i.e.
69 implementation-detail) subclass. Guaranteed to implement
70 the `collections.abc.Sized` (i.e. `__len__`) and
71 `collections.abc.Container` (i.e. `__contains__`) interfaces as
72 well as that of `DataCoordinateIterable`.
73 """
74 return _ScalarDataCoordinateIterable(dataId)
76 @property
77 @abstractmethod
78 def dimensions(self) -> DimensionGroup:
79 """Dimensions identified by these data IDs (`DimensionGroup`)."""
80 raise NotImplementedError()
82 @property
83 def universe(self) -> DimensionUniverse:
84 """Universe that defines all known compatible dimensions.
86 (`DimensionUniverse`).
87 """
88 return self.dimensions.universe
90 @abstractmethod
91 def hasFull(self) -> bool:
92 """Indicate if all data IDs in this iterable identify all dimensions.
94 Not just required dimensions.
96 Returns
97 -------
98 state : `bool`
99 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed.
100 If `False`, no guarantees are made.
101 """
102 raise NotImplementedError()
104 @abstractmethod
105 def hasRecords(self) -> bool:
106 """Return whether all data IDs in this iterable contain records.
108 Returns
109 -------
110 state : `bool`
111 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed.
112 If `False`, no guarantees are made.
113 """
114 raise NotImplementedError()
116 def toSet(self) -> DataCoordinateSet:
117 """Transform this iterable into a `DataCoordinateSet`.
119 Returns
120 -------
121 set : `DataCoordinateSet`
122 A `DataCoordinateSet` instance with the same elements as
123 ``self``, after removing any duplicates. May be ``self`` if it is
124 already a `DataCoordinateSet`.
125 """
126 return DataCoordinateSet(
127 frozenset(self),
128 dimensions=self.dimensions,
129 hasFull=self.hasFull(),
130 hasRecords=self.hasRecords(),
131 check=False,
132 )
134 def toSequence(self) -> DataCoordinateSequence:
135 """Transform this iterable into a `DataCoordinateSequence`.
137 Returns
138 -------
139 seq : `DataCoordinateSequence`
140 A new `DatasetCoordinateSequence` with the same elements as
141 ``self``, in the same order. May be ``self`` if it is already a
142 `DataCoordinateSequence`.
143 """
144 return DataCoordinateSequence(
145 tuple(self),
146 dimensions=self.dimensions,
147 hasFull=self.hasFull(),
148 hasRecords=self.hasRecords(),
149 check=False,
150 )
152 @abstractmethod
153 def subset(self, dimensions: DimensionGroup | Iterable[str]) -> DataCoordinateIterable:
154 """Return a subset iterable.
156 This subset iterable returns data IDs that identify a subset of the
157 dimensions that this one's do.
159 Parameters
160 ----------
161 dimensions : `DimensionGroup` or `~collections.abc.Iterable` [ `str` ]
162 Dimensions to be identified by the data IDs in the returned
163 iterable. Must be a subset of ``self.dimensions``.
165 Returns
166 -------
167 iterable : `DataCoordinateIterable`
168 A `DataCoordinateIterable` with
169 ``iterable.dimensions == dimensions``.
170 May be ``self`` if ``dimensions == self.dimensions``. Elements are
171 equivalent to those that would be created by calling
172 `DataCoordinate.subset` on all elements in ``self``, possibly
173 with deduplication and/or reordering (depending on the subclass,
174 which may make more specific guarantees).
175 """
176 raise NotImplementedError()
179class _ScalarDataCoordinateIterable(DataCoordinateIterable):
180 """An iterable for a single `DataCoordinate`.
182 A `DataCoordinateIterable` implementation that adapts a single
183 `DataCoordinate` instance.
185 This class should only be used directly by other code in the module in
186 which it is defined; all other code should interact with it only through
187 the `DataCoordinateIterable` interface.
189 Parameters
190 ----------
191 dataId : `DataCoordinate`
192 The data ID to adapt.
193 """
195 def __init__(self, dataId: DataCoordinate):
196 self._dataId = dataId
198 __slots__ = ("_dataId",)
200 def __iter__(self) -> Iterator[DataCoordinate]:
201 yield self._dataId
203 def __len__(self) -> int:
204 return 1
206 def __contains__(self, key: Any) -> bool:
207 if isinstance(key, DataCoordinate):
208 return key == self._dataId
209 else:
210 return False
212 @property
213 def dimensions(self) -> DimensionGroup:
214 # Docstring inherited from DataCoordinateIterable.
215 return self._dataId.dimensions
217 def hasFull(self) -> bool:
218 # Docstring inherited from DataCoordinateIterable.
219 return self._dataId.hasFull()
221 def hasRecords(self) -> bool:
222 # Docstring inherited from DataCoordinateIterable.
223 return self._dataId.hasRecords()
225 def subset(self, dimensions: DimensionGroup | Iterable[str]) -> _ScalarDataCoordinateIterable:
226 # Docstring inherited from DataCoordinateIterable.
227 dimensions = self.universe.conform(dimensions)
228 return _ScalarDataCoordinateIterable(self._dataId.subset(dimensions))
231class _DataCoordinateCollectionBase(DataCoordinateIterable):
232 """A partial iterable implementation backed by native Python collection.
234 A partial `DataCoordinateIterable` implementation that is backed by a
235 native Python collection.
237 This class is intended only to be used as an intermediate base class for
238 `DataCoordinateIterables` that assume a more specific type of collection
239 and can hence make more informed choices for how to implement some methods.
241 Parameters
242 ----------
243 dataIds : `collections.abc.Collection` [ `DataCoordinate` ]
244 A collection of `DataCoordinate` instances, with dimensions equal to
245 ``dimensions``.
246 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`
247 Dimensions identified by all data IDs in the collection.
248 hasFull : `bool`, optional
249 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
250 `True` for all given data IDs. If `False`, no such guarantee is made,
251 and `hasFull` will always return `False`. If `None` (default),
252 `hasFull` will be computed from the given data IDs, immediately if
253 ``check`` is `True`, or on first use if ``check`` is `False`.
254 hasRecords : `bool`, optional
255 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
256 returns `True` for all given data IDs. If `False`, no such guarantee
257 is made and `hasRecords` will always return `False`. If `None`
258 (default), `hasRecords` will be computed from the given data IDs,
259 immediately if ``check`` is `True`, or on first use if ``check`` is
260 `False`.
261 check: `bool`, optional
262 If `True` (default) check that all data IDs are consistent with the
263 given ``dimensions`` and state flags at construction. If `False`, no
264 checking will occur.
265 universe : `DimensionUniverse`
266 Object that manages all dimension definitions.
267 """
269 def __init__(
270 self,
271 dataIds: Collection[DataCoordinate],
272 *,
273 dimensions: Iterable[str] | DimensionGroup | None = None,
274 hasFull: bool | None = None,
275 hasRecords: bool | None = None,
276 check: bool = True,
277 universe: DimensionUniverse | None = None,
278 ):
279 universe = universe or getattr(dimensions, "universe", None) or getattr(dataIds, "universe", None)
280 if universe is None:
281 raise TypeError("universe must be provided, either directly or via dimensions or dataIds.")
282 if dimensions is not None:
283 dimensions = universe.conform(dimensions)
284 else:
285 raise TypeError("'dimensions' must be provided.")
286 self._dataIds = dataIds
287 self._dimensions = dimensions
288 if check:
289 for dataId in self._dataIds:
290 if hasFull and not dataId.hasFull():
291 raise ValueError(f"{dataId} is not complete, but is required to be.")
292 if hasRecords and not dataId.hasRecords():
293 raise ValueError(f"{dataId} has no records, but is required to.")
294 if dataId.dimensions != self._dimensions:
295 raise ValueError(f"Bad dimensions {dataId.dimensions}; expected {self._dimensions}.")
296 if hasFull is None:
297 hasFull = all(dataId.hasFull() for dataId in self._dataIds)
298 if hasRecords is None:
299 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
300 self._hasFull = hasFull
301 self._hasRecords = hasRecords
303 __slots__ = ("_dimensions", "_dataIds", "_hasFull", "_hasRecords")
305 @property
306 def dimensions(self) -> DimensionGroup:
307 # Docstring inherited from DataCoordinateIterable.
308 return self._dimensions
310 def hasFull(self) -> bool:
311 # Docstring inherited from DataCoordinateIterable.
312 if self._hasFull is None:
313 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds)
314 return self._hasFull
316 def hasRecords(self) -> bool:
317 # Docstring inherited from DataCoordinateIterable.
318 if self._hasRecords is None:
319 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds)
320 return self._hasRecords
322 def toSet(self) -> DataCoordinateSet:
323 # Docstring inherited from DataCoordinateIterable.
324 # Override base class to pass in attributes instead of results of
325 # method calls for _hasFull and _hasRecords - those can be None,
326 # and hence defer checking if that's what the user originally wanted.
327 return DataCoordinateSet(
328 frozenset(self._dataIds),
329 dimensions=self._dimensions,
330 hasFull=self._hasFull,
331 hasRecords=self._hasRecords,
332 check=False,
333 )
335 def toSequence(self) -> DataCoordinateSequence:
336 # Docstring inherited from DataCoordinateIterable.
337 # Override base class to pass in attributes instead of results of
338 # method calls for _hasFull and _hasRecords - those can be None,
339 # and hence defer checking if that's what the user originally wanted.
340 return DataCoordinateSequence(
341 tuple(self._dataIds),
342 dimensions=self._dimensions,
343 hasFull=self._hasFull,
344 hasRecords=self._hasRecords,
345 check=False,
346 )
348 def __iter__(self) -> Iterator[DataCoordinate]:
349 return iter(self._dataIds)
351 def __len__(self) -> int:
352 return len(self._dataIds)
354 def __contains__(self, key: Any) -> bool:
355 key = DataCoordinate.standardize(key, universe=self.universe)
356 return key in self._dataIds
358 def _subsetKwargs(self, dimensions: DimensionGroup) -> dict[str, Any]:
359 """Return constructor kwargs useful for subclasses implementing subset.
361 Parameters
362 ----------
363 dimensions : `DimensionGroup`
364 Dimensions passed to `subset`.
366 Returns
367 -------
368 **kwargs
369 A dict with `hasFull`, `hasRecords`, and `check` keys, associated
370 with the appropriate values for a `subset` operation with the given
371 dimensions.
372 """
373 hasFull: bool | None
374 if dimensions.names <= self.dimensions.required:
375 hasFull = True
376 else:
377 hasFull = self._hasFull
378 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False)
381class DataCoordinateSet(_DataCoordinateCollectionBase):
382 """Iterable iteration that is set-like.
384 A `DataCoordinateIterable` implementation that adds some set-like
385 functionality, and is backed by a true set-like object.
387 Parameters
388 ----------
389 dataIds : `collections.abc.Set` [ `DataCoordinate` ]
390 A set of `DataCoordinate` instances, with dimensions equal to
391 ``dimensions``. If this is a mutable object, the caller must be able
392 to guarantee that it will not be modified by any other holders.
393 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`
394 Dimensions identified by all data IDs in the collection.
395 hasFull : `bool`, optional
396 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
397 `True` for all given data IDs. If `False`, no such guarantee is made,
398 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
399 (default), `DataCoordinateSet.hasFull` will be computed from the given
400 data IDs, immediately if ``check`` is `True`, or on first use if
401 ``check`` is `False`.
402 hasRecords : `bool`, optional
403 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
404 returns `True` for all given data IDs. If `False`, no such guarantee
405 is made and `DataCoordinateSet.hasRecords` will always return `False`.
406 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
407 from the given data IDs, immediately if ``check`` is `True`, or on
408 first use if ``check`` is `False`.
409 check : `bool`, optional
410 If `True` (default) check that all data IDs are consistent with the
411 given ``dimensions`` and state flags at construction. If `False`, no
412 checking will occur.
413 universe : `DimensionUniverse`
414 Object that manages all dimension definitions.
416 Notes
417 -----
418 `DataCoordinateSet` does not formally implement the `collections.abc.Set`
419 interface, because that requires many binary operations to accept any
420 set-like object as the other argument (regardless of what its elements
421 might be), and it's much easier to ensure those operations never behave
422 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes)
423 `DataCoordinateIterable`, and in most cases restrict that they identify
424 the same dimensions. In particular:
426 - a `DataCoordinateSet` will compare as not equal to any object that is
427 not a `DataCoordinateSet`, even native Python sets containing the exact
428 same elements;
430 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``)
431 require both operands to be `DataCoordinateSet` instances that have the
432 same dimensions (i.e. `dimensions` attribute);
434 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to
435 be a `DataCoordinateIterable` with the same dimensions;
437 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both
438 operands to be `DataCoordinateSet` instances that have the same
439 dimensions _and_ the same ``dtype``;
441 - named methods that create new sets (`intersection`, `union`,
442 `symmetric_difference`, `difference`) require the other operand to be a
443 `DataCoordinateIterable` with the same dimensions _and_ the same
444 ``dtype``.
446 In addition, when the two operands differ in the return values of `hasFull`
447 and/or `hasRecords`, we make no guarantees about what those methods will
448 return on the new `DataCoordinateSet` (other than that they will accurately
449 reflect what elements are in the new set - we just don't control which
450 elements are contributed by each operand).
451 """
453 def __init__(
454 self,
455 dataIds: Set[DataCoordinate],
456 *,
457 dimensions: Iterable[str] | DimensionGroup | None = None,
458 hasFull: bool | None = None,
459 hasRecords: bool | None = None,
460 check: bool = True,
461 universe: DimensionUniverse | None = None,
462 ):
463 super().__init__(
464 dataIds,
465 dimensions=dimensions,
466 hasFull=hasFull,
467 hasRecords=hasRecords,
468 check=check,
469 universe=universe,
470 )
472 _dataIds: Set[DataCoordinate]
474 __slots__ = ()
476 def __str__(self) -> str:
477 return str(set(self._dataIds))
479 def __repr__(self) -> str:
480 return (
481 f"DataCoordinateSet({set(self._dataIds)}, {self._dimensions!r}, "
482 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})"
483 )
485 def __eq__(self, other: Any) -> bool:
486 if isinstance(other, DataCoordinateSet):
487 return self._dimensions == other._dimensions and self._dataIds == other._dataIds
488 return False
490 def __le__(self, other: DataCoordinateSet) -> bool:
491 if self.dimensions != other.dimensions:
492 raise ValueError(
493 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
494 )
495 return self._dataIds <= other._dataIds
497 def __ge__(self, other: DataCoordinateSet) -> bool:
498 if self.dimensions != other.dimensions:
499 raise ValueError(
500 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
501 )
502 return self._dataIds >= other._dataIds
504 def __lt__(self, other: DataCoordinateSet) -> bool:
505 if self.dimensions != other.dimensions:
506 raise ValueError(
507 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
508 )
509 return self._dataIds < other._dataIds
511 def __gt__(self, other: DataCoordinateSet) -> bool:
512 if self.dimensions != other.dimensions:
513 raise ValueError(
514 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
515 )
516 return self._dataIds > other._dataIds
518 def issubset(self, other: DataCoordinateIterable) -> bool:
519 """Test whether ``self`` contains all data IDs in ``other``.
521 Parameters
522 ----------
523 other : `DataCoordinateIterable`
524 An iterable of data IDs with
525 ``other.dimensions == self.dimensions``.
527 Returns
528 -------
529 issubset : `bool`
530 `True` if all data IDs in ``self`` are also in ``other``, and
531 `False` otherwise.
532 """
533 if self.dimensions != other.dimensions:
534 raise ValueError(
535 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
536 )
537 return self._dataIds <= other.toSet()._dataIds
539 def issuperset(self, other: DataCoordinateIterable) -> bool:
540 """Test whether ``other`` contains all data IDs in ``self``.
542 Parameters
543 ----------
544 other : `DataCoordinateIterable`
545 An iterable of data IDs with
546 ``other.dimensions == self.dimensions``.
548 Returns
549 -------
550 issuperset : `bool`
551 `True` if all data IDs in ``other`` are also in ``self``, and
552 `False` otherwise.
553 """
554 if self.dimensions != other.dimensions:
555 raise ValueError(
556 f"Inconsistent dimensions in set comparision: {self.dimensions} != {other.dimensions}."
557 )
558 return self._dataIds >= other.toSet()._dataIds
560 def isdisjoint(self, other: DataCoordinateIterable) -> bool:
561 """Test whether there are no data IDs in both ``self`` and ``other``.
563 Parameters
564 ----------
565 other : `DataCoordinateIterable`
566 An iterable of data IDs with
567 ``other._dimensions == self._dimensions``.
569 Returns
570 -------
571 isdisjoint : `bool`
572 `True` if there are no data IDs in both ``self`` and ``other``, and
573 `False` otherwise.
574 """
575 if self._dimensions != other.dimensions:
576 raise ValueError(
577 f"Inconsistent dimensions in set comparision: {self._dimensions} != {other.dimensions}."
578 )
579 return self._dataIds.isdisjoint(other.toSet()._dataIds)
581 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet:
582 if self._dimensions != other.dimensions:
583 raise ValueError(
584 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
585 )
586 return DataCoordinateSet(self._dataIds & other._dataIds, dimensions=self._dimensions, check=False)
588 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet:
589 if self._dimensions != other.dimensions:
590 raise ValueError(
591 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
592 )
593 return DataCoordinateSet(self._dataIds | other._dataIds, dimensions=self._dimensions, check=False)
595 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet:
596 if self._dimensions != other.dimensions:
597 raise ValueError(
598 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
599 )
600 return DataCoordinateSet(self._dataIds ^ other._dataIds, dimensions=self._dimensions, check=False)
602 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet:
603 if self._dimensions != other.dimensions:
604 raise ValueError(
605 f"Inconsistent dimensions in set operation: {self._dimensions} != {other.dimensions}."
606 )
607 return DataCoordinateSet(self._dataIds - other._dataIds, dimensions=self._dimensions, check=False)
609 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet:
610 """Return a new set that contains all data IDs from parameters.
612 Parameters
613 ----------
614 other : `DataCoordinateIterable`
615 An iterable of data IDs with
616 ``other.dimensions == self.dimensions``.
618 Returns
619 -------
620 intersection : `DataCoordinateSet`
621 A new `DataCoordinateSet` instance.
622 """
623 if self.dimensions != other.dimensions:
624 raise ValueError(
625 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
626 )
627 return DataCoordinateSet(
628 self._dataIds & other.toSet()._dataIds, dimensions=self.dimensions, check=False
629 )
631 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet:
632 """Return a new set that contains all data IDs in either parameters.
634 Parameters
635 ----------
636 other : `DataCoordinateIterable`
637 An iterable of data IDs with
638 ``other.dimensions == self.dimensions``.
640 Returns
641 -------
642 intersection : `DataCoordinateSet`
643 A new `DataCoordinateSet` instance.
644 """
645 if self.dimensions != other.dimensions:
646 raise ValueError(
647 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
648 )
649 return DataCoordinateSet(
650 self._dataIds | other.toSet()._dataIds, dimensions=self.dimensions, check=False
651 )
653 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
654 """Return a new set with all data IDs in either parameters, not both.
656 Parameters
657 ----------
658 other : `DataCoordinateIterable`
659 An iterable of data IDs with
660 ``other.dimensions == self.dimensions``.
662 Returns
663 -------
664 intersection : `DataCoordinateSet`
665 A new `DataCoordinateSet` instance.
666 """
667 if self.dimensions != other.dimensions:
668 raise ValueError(
669 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
670 )
671 return DataCoordinateSet(
672 self._dataIds ^ other.toSet()._dataIds, dimensions=self.dimensions, check=False
673 )
675 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet:
676 """Return a new set with all data IDs in this that are not in other.
678 Parameters
679 ----------
680 other : `DataCoordinateIterable`
681 An iterable of data IDs with
682 ``other.dimensions == self.dimensions``.
684 Returns
685 -------
686 intersection : `DataCoordinateSet`
687 A new `DataCoordinateSet` instance.
688 """
689 if self.dimensions != other.dimensions:
690 raise ValueError(
691 f"Inconsistent dimensions in set operation: {self.dimensions} != {other.dimensions}."
692 )
693 return DataCoordinateSet(
694 self._dataIds - other.toSet()._dataIds, dimensions=self.dimensions, check=False
695 )
697 def toSet(self) -> DataCoordinateSet:
698 # Docstring inherited from DataCoordinateIterable.
699 return self
701 def subset(self, dimensions: DimensionGroup | Iterable[str]) -> DataCoordinateSet:
702 """Return a set whose data IDs identify a subset.
704 Parameters
705 ----------
706 dimensions : `DimensionGroup` or `~collections.abc.Iterable` [ `str` ]
707 Dimensions to be identified by the data IDs in the returned
708 iterable. Must be a subset of ``self.dimensions``.
710 Returns
711 -------
712 set : `DataCoordinateSet`
713 A `DataCoordinateSet` with ``set.dimensions == dimensions``. Will
714 be ``self`` if ``dimensions == self.dimensions``. Elements are
715 equivalent to those that would be created by calling
716 `DataCoordinate.subset` on all elements in ``self``, with
717 deduplication and in arbitrary order.
718 """
719 dimensions = self.universe.conform(dimensions)
720 if dimensions == self.dimensions:
721 return self
722 return DataCoordinateSet(
723 {dataId.subset(dimensions) for dataId in self._dataIds},
724 dimensions=dimensions,
725 **self._subsetKwargs(dimensions),
726 )
729class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]):
730 """Iterable supporting the full Sequence interface.
732 A `DataCoordinateIterable` implementation that supports the full
733 `collections.abc.Sequence` interface.
735 Parameters
736 ----------
737 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ]
738 A sequence of `DataCoordinate` instances, with dimensions equal to
739 ``dimensions``.
740 dimensions : `~collections.abc.Iterable` [ `str` ], `DimensionGroup`
741 Dimensions identified by all data IDs in the collection.
742 hasFull : `bool`, optional
743 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns
744 `True` for all given data IDs. If `False`, no such guarantee is made,
745 and `DataCoordinateSet.hasFull` will always return `False`. If `None`
746 (default), `DataCoordinateSet.hasFull` will be computed from the given
747 data IDs, immediately if ``check`` is `True`, or on first use if
748 ``check`` is `False`.
749 hasRecords : `bool`, optional
750 If `True`, the caller guarantees that `DataCoordinate.hasRecords`
751 returns `True` for all given data IDs. If `False`, no such guarantee
752 is made and `DataCoordinateSet.hasRecords` will always return `False`.
753 If `None` (default), `DataCoordinateSet.hasRecords` will be computed
754 from the given data IDs, immediately if ``check`` is `True`, or on
755 first use if ``check`` is `False`.
756 check : `bool`, optional
757 If `True` (default) check that all data IDs are consistent with the
758 given ``dimensions`` and state flags at construction. If `False`, no
759 checking will occur.
760 universe : `DimensionUniverse`
761 Object that manages all dimension definitions.
762 """
764 def __init__(
765 self,
766 dataIds: Sequence[DataCoordinate],
767 *,
768 dimensions: Iterable[str] | DimensionGroup | None = None,
769 hasFull: bool | None = None,
770 hasRecords: bool | None = None,
771 check: bool = True,
772 universe: DimensionUniverse | None = None,
773 ):
774 super().__init__(
775 tuple(dataIds),
776 dimensions=dimensions,
777 hasFull=hasFull,
778 hasRecords=hasRecords,
779 check=check,
780 universe=universe,
781 )
783 _dataIds: Sequence[DataCoordinate]
785 __slots__ = ()
787 def __str__(self) -> str:
788 return str(tuple(self._dataIds))
790 def __repr__(self) -> str:
791 return (
792 f"DataCoordinateSequence({tuple(self._dataIds)}, {self._dimensions!r}, "
793 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})"
794 )
796 def __eq__(self, other: Any) -> bool:
797 if isinstance(other, DataCoordinateSequence):
798 return self._dimensions == other._dimensions and self._dataIds == other._dataIds
799 return False
801 @overload
802 def __getitem__(self, index: int) -> DataCoordinate:
803 pass
805 @overload
806 def __getitem__(self, index: slice) -> DataCoordinateSequence:
807 pass
809 def __getitem__(self, index: Any) -> Any:
810 r = self._dataIds[index]
811 if isinstance(index, slice):
812 return DataCoordinateSequence(
813 r,
814 dimensions=self._dimensions,
815 hasFull=self._hasFull,
816 hasRecords=self._hasRecords,
817 check=False,
818 )
819 return r
821 def toSequence(self) -> DataCoordinateSequence:
822 # Docstring inherited from DataCoordinateIterable.
823 return self
825 def subset(self, dimensions: DimensionGroup | Iterable[str]) -> DataCoordinateSequence:
826 """Return a sequence whose data IDs identify a subset.
828 Parameters
829 ----------
830 dimensions : `DimensionGroup` or `~collections.abc.Iterable` [ `str` ]
831 Dimensions to be identified by the data IDs in the returned
832 iterable. Must be a subset of ``self.dimensions``.
834 Returns
835 -------
836 set : `DataCoordinateSequence`
837 A `DataCoordinateSequence` with ``set.dimensions == dimensions``.
838 Will be ``self`` if ``dimensions == self.dimensions``. Elements
839 are equivalent to those that would be created by calling
840 `DataCoordinate.subset` on all elements in ``self``, in the same
841 order and with no deduplication.
842 """
843 dimensions = self.universe.conform(dimensions)
844 if dimensions == self.dimensions:
845 return self
846 return DataCoordinateSequence(
847 tuple(dataId.subset(dimensions) for dataId in self._dataIds),
848 dimensions=dimensions,
849 **self._subsetKwargs(dimensions),
850 )