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-05-01 08:18 +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 "DataCoordinateSequence", 

33 "DataCoordinateSet", 

34) 

35 

36from abc import abstractmethod 

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

38from typing import Any, overload 

39 

40from ._coordinate import DataCoordinate 

41from ._group import DimensionGroup 

42from ._universe import DimensionUniverse 

43 

44 

45class DataCoordinateIterable(Iterable[DataCoordinate]): 

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

47 

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

52 

53 __slots__ = () 

54 

55 @staticmethod 

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

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

58 

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. 

64 

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) 

75 

76 @property 

77 @abstractmethod 

78 def dimensions(self) -> DimensionGroup: 

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

80 raise NotImplementedError() 

81 

82 @property 

83 def universe(self) -> DimensionUniverse: 

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

85 

86 (`DimensionUniverse`). 

87 """ 

88 return self.dimensions.universe 

89 

90 @abstractmethod 

91 def hasFull(self) -> bool: 

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

93 

94 Not just required dimensions. 

95 

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

103 

104 @abstractmethod 

105 def hasRecords(self) -> bool: 

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

107 

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

115 

116 def toSet(self) -> DataCoordinateSet: 

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

118 

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 ) 

133 

134 def toSequence(self) -> DataCoordinateSequence: 

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

136 

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 ) 

151 

152 @abstractmethod 

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

154 """Return a subset iterable. 

155 

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

157 dimensions that this one's do. 

158 

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

164 

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

177 

178 

179class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

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

181 

182 A `DataCoordinateIterable` implementation that adapts a single 

183 `DataCoordinate` instance. 

184 

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. 

188 

189 Parameters 

190 ---------- 

191 dataId : `DataCoordinate` 

192 The data ID to adapt. 

193 """ 

194 

195 def __init__(self, dataId: DataCoordinate): 

196 self._dataId = dataId 

197 

198 __slots__ = ("_dataId",) 

199 

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

201 yield self._dataId 

202 

203 def __len__(self) -> int: 

204 return 1 

205 

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

207 if isinstance(key, DataCoordinate): 

208 return key == self._dataId 

209 else: 

210 return False 

211 

212 @property 

213 def dimensions(self) -> DimensionGroup: 

214 # Docstring inherited from DataCoordinateIterable. 

215 return self._dataId.dimensions 

216 

217 def hasFull(self) -> bool: 

218 # Docstring inherited from DataCoordinateIterable. 

219 return self._dataId.hasFull() 

220 

221 def hasRecords(self) -> bool: 

222 # Docstring inherited from DataCoordinateIterable. 

223 return self._dataId.hasRecords() 

224 

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

229 

230 

231class _DataCoordinateCollectionBase(DataCoordinateIterable): 

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

233 

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

235 native Python collection. 

236 

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. 

240 

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

268 

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 

302 

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

304 

305 @property 

306 def dimensions(self) -> DimensionGroup: 

307 # Docstring inherited from DataCoordinateIterable. 

308 return self._dimensions 

309 

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 

315 

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 

321 

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 ) 

334 

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 ) 

347 

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

349 return iter(self._dataIds) 

350 

351 def __len__(self) -> int: 

352 return len(self._dataIds) 

353 

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

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

356 return key in self._dataIds 

357 

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

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

360 

361 Parameters 

362 ---------- 

363 dimensions : `DimensionGroup` 

364 Dimensions passed to `subset`. 

365 

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) 

379 

380 

381class DataCoordinateSet(_DataCoordinateCollectionBase): 

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

383 

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

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

386 

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. 

415 

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: 

425 

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; 

429 

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

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

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

433 

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

435 be a `DataCoordinateIterable` with the same dimensions; 

436 

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

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

439 dimensions _and_ the same ``dtype``; 

440 

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

445 

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

452 

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 ) 

471 

472 _dataIds: Set[DataCoordinate] 

473 

474 __slots__ = () 

475 

476 def __str__(self) -> str: 

477 return str(set(self._dataIds)) 

478 

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 ) 

484 

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 

489 

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 

496 

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 

503 

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 

510 

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 

517 

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

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

520 

521 Parameters 

522 ---------- 

523 other : `DataCoordinateIterable` 

524 An iterable of data IDs with 

525 ``other.dimensions == self.dimensions``. 

526 

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 

538 

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

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

541 

542 Parameters 

543 ---------- 

544 other : `DataCoordinateIterable` 

545 An iterable of data IDs with 

546 ``other.dimensions == self.dimensions``. 

547 

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 

559 

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

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

562 

563 Parameters 

564 ---------- 

565 other : `DataCoordinateIterable` 

566 An iterable of data IDs with 

567 ``other._dimensions == self._dimensions``. 

568 

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) 

580 

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) 

587 

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) 

594 

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) 

601 

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) 

608 

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

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

611 

612 Parameters 

613 ---------- 

614 other : `DataCoordinateIterable` 

615 An iterable of data IDs with 

616 ``other.dimensions == self.dimensions``. 

617 

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 ) 

630 

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

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

633 

634 Parameters 

635 ---------- 

636 other : `DataCoordinateIterable` 

637 An iterable of data IDs with 

638 ``other.dimensions == self.dimensions``. 

639 

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 ) 

652 

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

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

655 

656 Parameters 

657 ---------- 

658 other : `DataCoordinateIterable` 

659 An iterable of data IDs with 

660 ``other.dimensions == self.dimensions``. 

661 

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 ) 

674 

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

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

677 

678 Parameters 

679 ---------- 

680 other : `DataCoordinateIterable` 

681 An iterable of data IDs with 

682 ``other.dimensions == self.dimensions``. 

683 

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 ) 

696 

697 def toSet(self) -> DataCoordinateSet: 

698 # Docstring inherited from DataCoordinateIterable. 

699 return self 

700 

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

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

703 

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

709 

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 ) 

727 

728 

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

730 """Iterable supporting the full Sequence interface. 

731 

732 A `DataCoordinateIterable` implementation that supports the full 

733 `collections.abc.Sequence` interface. 

734 

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

763 

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 ) 

782 

783 _dataIds: Sequence[DataCoordinate] 

784 

785 __slots__ = () 

786 

787 def __str__(self) -> str: 

788 return str(tuple(self._dataIds)) 

789 

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 ) 

795 

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 

800 

801 @overload 

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

803 pass 

804 

805 @overload 

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

807 pass 

808 

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 

820 

821 def toSequence(self) -> DataCoordinateSequence: 

822 # Docstring inherited from DataCoordinateIterable. 

823 return self 

824 

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

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

827 

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

833 

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 )