Coverage for python/lsst/daf/butler/dimensions/_data_coordinate_iterable.py: 36%

228 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-13 09:58 +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/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ( 

31 "DataCoordinateIterable", 

32 "DataCoordinateSet", 

33 "DataCoordinateSequence", 

34) 

35 

36import warnings 

37from abc import abstractmethod 

38from collections.abc import Collection, Iterable, Iterator, Sequence, Set 

39from typing import Any, overload 

40 

41from deprecated.sphinx import deprecated 

42from lsst.utils.introspection import find_outside_stacklevel 

43 

44from ._coordinate import DataCoordinate 

45from ._graph import DimensionGraph 

46from ._group import DimensionGroup 

47from ._universe import DimensionUniverse 

48 

49 

50class DataCoordinateIterable(Iterable[DataCoordinate]): 

51 """An abstract base class for homogeneous iterables of data IDs. 

52 

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 """ 

57 

58 __slots__ = () 

59 

60 @staticmethod 

61 def fromScalar(dataId: DataCoordinate) -> _ScalarDataCoordinateIterable: 

62 """Return a `DataCoordinateIterable` containing the single data ID. 

63 

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. 

69 

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) 

80 

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() 

91 

92 @property 

93 @abstractmethod 

94 def dimensions(self) -> DimensionGroup: 

95 """Dimensions identified by these data IDs (`DimensionGroup`).""" 

96 raise NotImplementedError() 

97 

98 @property 

99 def universe(self) -> DimensionUniverse: 

100 """Universe that defines all known compatible dimensions. 

101 

102 (`DimensionUniverse`). 

103 """ 

104 return self.dimensions.universe 

105 

106 @abstractmethod 

107 def hasFull(self) -> bool: 

108 """Indicate if all data IDs in this iterable identify all dimensions. 

109 

110 Not just required dimensions. 

111 

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() 

119 

120 @abstractmethod 

121 def hasRecords(self) -> bool: 

122 """Return whether all data IDs in this iterable contain records. 

123 

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() 

131 

132 def toSet(self) -> DataCoordinateSet: 

133 """Transform this iterable into a `DataCoordinateSet`. 

134 

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 ) 

149 

150 def toSequence(self) -> DataCoordinateSequence: 

151 """Transform this iterable into a `DataCoordinateSequence`. 

152 

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 ) 

167 

168 @abstractmethod 

169 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateIterable: 

170 """Return a subset iterable. 

171 

172 This subset iterable returns data IDs that identify a subset of the 

173 dimensions that this one's do. 

174 

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``. 

181 

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() 

194 

195 

196class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

197 """An iterable for a single `DataCoordinate`. 

198 

199 A `DataCoordinateIterable` implementation that adapts a single 

200 `DataCoordinate` instance. 

201 

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. 

205 

206 Parameters 

207 ---------- 

208 dataId : `DataCoordinate` 

209 The data ID to adapt. 

210 """ 

211 

212 def __init__(self, dataId: DataCoordinate): 

213 self._dataId = dataId 

214 

215 __slots__ = ("_dataId",) 

216 

217 def __iter__(self) -> Iterator[DataCoordinate]: 

218 yield self._dataId 

219 

220 def __len__(self) -> int: 

221 return 1 

222 

223 def __contains__(self, key: Any) -> bool: 

224 if isinstance(key, DataCoordinate): 

225 return key == self._dataId 

226 else: 

227 return False 

228 

229 @property 

230 def dimensions(self) -> DimensionGroup: 

231 # Docstring inherited from DataCoordinateIterable. 

232 return self._dataId.dimensions 

233 

234 def hasFull(self) -> bool: 

235 # Docstring inherited from DataCoordinateIterable. 

236 return self._dataId.hasFull() 

237 

238 def hasRecords(self) -> bool: 

239 # Docstring inherited from DataCoordinateIterable. 

240 return self._dataId.hasRecords() 

241 

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)) 

248 

249 

250class _DataCoordinateCollectionBase(DataCoordinateIterable): 

251 """A partial iterable implementation backed by native Python collection. 

252 

253 A partial `DataCoordinateIterable` implementation that is backed by a 

254 native Python collection. 

255 

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. 

259 

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 """ 

292 

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 

345 

346 __slots__ = ("_dimensions", "_dataIds", "_hasFull", "_hasRecords") 

347 

348 @property 

349 def dimensions(self) -> DimensionGroup: 

350 # Docstring inherited from DataCoordinateIterable. 

351 return self._dimensions 

352 

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 

358 

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 

364 

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 ) 

377 

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 ) 

390 

391 def __iter__(self) -> Iterator[DataCoordinate]: 

392 return iter(self._dataIds) 

393 

394 def __len__(self) -> int: 

395 return len(self._dataIds) 

396 

397 def __contains__(self, key: Any) -> bool: 

398 key = DataCoordinate.standardize(key, universe=self.universe) 

399 return key in self._dataIds 

400 

401 def _subsetKwargs(self, dimensions: DimensionGroup) -> dict[str, Any]: 

402 """Return constructor kwargs useful for subclasses implementing subset. 

403 

404 Parameters 

405 ---------- 

406 dimensions : `DimensionGroup` 

407 Dimensions passed to `subset`. 

408 

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) 

422 

423 

424class DataCoordinateSet(_DataCoordinateCollectionBase): 

425 """Iterable iteration that is set-like. 

426 

427 A `DataCoordinateIterable` implementation that adds some set-like 

428 functionality, and is backed by a true set-like object. 

429 

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. 

463 

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: 

473 

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; 

477 

478 - subset/superset comparison _operators_ (``<``, ``>``, ``<=``, ``>=``) 

479 require both operands to be `DataCoordinateSet` instances that have the 

480 same dimensions (i.e. `dimensions` attribute); 

481 

482 - `issubset`, `issuperset`, and `isdisjoint` require the other argument to 

483 be a `DataCoordinateIterable` with the same dimensions; 

484 

485 - operators that create new sets (``&``, ``|``, ``^``, ``-``) require both 

486 operands to be `DataCoordinateSet` instances that have the same 

487 dimensions _and_ the same ``dtype``; 

488 

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``. 

493 

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 """ 

500 

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 ) 

521 

522 _dataIds: Set[DataCoordinate] 

523 

524 __slots__ = () 

525 

526 def __str__(self) -> str: 

527 return str(set(self._dataIds)) 

528 

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 ) 

534 

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 

539 

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 

546 

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 

553 

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 

560 

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 

567 

568 def issubset(self, other: DataCoordinateIterable) -> bool: 

569 """Test whether ``self`` contains all data IDs in ``other``. 

570 

571 Parameters 

572 ---------- 

573 other : `DataCoordinateIterable` 

574 An iterable of data IDs with ``other.graph == self.graph``. 

575 

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 

587 

588 def issuperset(self, other: DataCoordinateIterable) -> bool: 

589 """Test whether ``other`` contains all data IDs in ``self``. 

590 

591 Parameters 

592 ---------- 

593 other : `DataCoordinateIterable` 

594 An iterable of data IDs with 

595 ``other.dimensions == self.dimensions``. 

596 

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 

608 

609 def isdisjoint(self, other: DataCoordinateIterable) -> bool: 

610 """Test whether there are no data IDs in both ``self`` and ``other``. 

611 

612 Parameters 

613 ---------- 

614 other : `DataCoordinateIterable` 

615 An iterable of data IDs with 

616 ``other._dimensions == self._dimensions``. 

617 

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) 

629 

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) 

636 

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) 

643 

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) 

650 

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) 

657 

658 def intersection(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

659 """Return a new set that contains all data IDs from parameters. 

660 

661 Parameters 

662 ---------- 

663 other : `DataCoordinateIterable` 

664 An iterable of data IDs with 

665 ``other.dimensions == self.dimensions``. 

666 

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 ) 

679 

680 def union(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

681 """Return a new set that contains all data IDs in either parameters. 

682 

683 Parameters 

684 ---------- 

685 other : `DataCoordinateIterable` 

686 An iterable of data IDs with 

687 ``other.dimensions == self.dimensions``. 

688 

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 ) 

701 

702 def symmetric_difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

703 """Return a new set with all data IDs in either parameters, not both. 

704 

705 Parameters 

706 ---------- 

707 other : `DataCoordinateIterable` 

708 An iterable of data IDs with 

709 ``other.dimensions == self.dimensions``. 

710 

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 ) 

723 

724 def difference(self, other: DataCoordinateIterable) -> DataCoordinateSet: 

725 """Return a new set with all data IDs in this that are not in other. 

726 

727 Parameters 

728 ---------- 

729 other : `DataCoordinateIterable` 

730 An iterable of data IDs with 

731 ``other.dimensions == self.dimensions``. 

732 

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 ) 

745 

746 def toSet(self) -> DataCoordinateSet: 

747 # Docstring inherited from DataCoordinateIterable. 

748 return self 

749 

750 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateSet: 

751 """Return a set whose data IDs identify a subset. 

752 

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``. 

759 

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 ) 

777 

778 

779class DataCoordinateSequence(_DataCoordinateCollectionBase, Sequence[DataCoordinate]): 

780 """Iterable supporting the full Sequence interface. 

781 

782 A `DataCoordinateIterable` implementation that supports the full 

783 `collections.abc.Sequence` interface. 

784 

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 """ 

818 

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 ) 

839 

840 _dataIds: Sequence[DataCoordinate] 

841 

842 __slots__ = () 

843 

844 def __str__(self) -> str: 

845 return str(tuple(self._dataIds)) 

846 

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 ) 

852 

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 

857 

858 @overload 

859 def __getitem__(self, index: int) -> DataCoordinate: 

860 pass 

861 

862 @overload 

863 def __getitem__(self, index: slice) -> DataCoordinateSequence: 

864 pass 

865 

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 

877 

878 def toSequence(self) -> DataCoordinateSequence: 

879 # Docstring inherited from DataCoordinateIterable. 

880 return self 

881 

882 def subset(self, dimensions: DimensionGraph | DimensionGroup | Iterable[str]) -> DataCoordinateSequence: 

883 """Return a sequence whose data IDs identify a subset. 

884 

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``. 

891 

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 )