Coverage for python/lsst/daf/butler/core/dimensions/_dataCoordinateIterable.py: 37%

205 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-14 19:21 +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 program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "DataCoordinateIterable", 

26 "DataCoordinateSet", 

27 "DataCoordinateSequence", 

28) 

29 

30from abc import abstractmethod 

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

32from typing import Any, overload 

33 

34from ._coordinate import DataCoordinate 

35from ._graph import DimensionGraph 

36from ._universe import DimensionUniverse 

37 

38 

39class DataCoordinateIterable(Iterable[DataCoordinate]): 

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

41 

42 All elements of a `DataCoordinateIterable` identify the same set of 

43 dimensions (given by the `graph` property) and generally have the same 

44 `DataCoordinate.hasFull` and `DataCoordinate.hasRecords` flag values. 

45 """ 

46 

47 __slots__ = () 

48 

49 @staticmethod 

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

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

52 

53 Parameters 

54 ---------- 

55 dataId : `DataCoordinate` 

56 Data ID to adapt. Must be a true `DataCoordinate` instance, not 

57 an arbitrary mapping. No runtime checking is performed. 

58 

59 Returns 

60 ------- 

61 iterable : `DataCoordinateIterable` 

62 A `DataCoordinateIterable` instance of unspecified (i.e. 

63 implementation-detail) subclass. Guaranteed to implement 

64 the `collections.abc.Sized` (i.e. `__len__`) and 

65 `collections.abc.Container` (i.e. `__contains__`) interfaces as 

66 well as that of `DataCoordinateIterable`. 

67 """ 

68 return _ScalarDataCoordinateIterable(dataId) 

69 

70 @property 

71 @abstractmethod 

72 def graph(self) -> DimensionGraph: 

73 """Dimensions identified by these data IDs (`DimensionGraph`).""" 

74 raise NotImplementedError() 

75 

76 @property 

77 def universe(self) -> DimensionUniverse: 

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

79 

80 (`DimensionUniverse`). 

81 """ 

82 return self.graph.universe 

83 

84 @abstractmethod 

85 def hasFull(self) -> bool: 

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

87 

88 Not just required dimensions. 

89 

90 Returns 

91 ------- 

92 state : `bool` 

93 If `True`, ``all(d.hasFull() for d in iterable)`` is guaranteed. 

94 If `False`, no guarantees are made. 

95 """ 

96 raise NotImplementedError() 

97 

98 @abstractmethod 

99 def hasRecords(self) -> bool: 

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

101 

102 Returns 

103 ------- 

104 state : `bool` 

105 If `True`, ``all(d.hasRecords() for d in iterable)`` is guaranteed. 

106 If `False`, no guarantees are made. 

107 """ 

108 raise NotImplementedError() 

109 

110 def toSet(self) -> DataCoordinateSet: 

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

112 

113 Returns 

114 ------- 

115 set : `DataCoordinateSet` 

116 A `DatasetCoordinateSet` instance with the same elements as 

117 ``self``, after removing any duplicates. May be ``self`` if it is 

118 already a `DataCoordinateSet`. 

119 """ 

120 return DataCoordinateSet( 

121 frozenset(self), 

122 graph=self.graph, 

123 hasFull=self.hasFull(), 

124 hasRecords=self.hasRecords(), 

125 check=False, 

126 ) 

127 

128 def toSequence(self) -> DataCoordinateSequence: 

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

130 

131 Returns 

132 ------- 

133 seq : `DataCoordinateSequence` 

134 A new `DatasetCoordinateSequence` with the same elements as 

135 ``self``, in the same order. May be ``self`` if it is already a 

136 `DataCoordinateSequence`. 

137 """ 

138 return DataCoordinateSequence( 

139 tuple(self), graph=self.graph, hasFull=self.hasFull(), hasRecords=self.hasRecords(), check=False 

140 ) 

141 

142 @abstractmethod 

143 def subset(self, graph: DimensionGraph) -> DataCoordinateIterable: 

144 """Return a subset iterable. 

145 

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

147 dimensions that this one's do. 

148 

149 Parameters 

150 ---------- 

151 graph : `DimensionGraph` 

152 Dimensions to be identified by the data IDs in the returned 

153 iterable. Must be a subset of ``self.graph``. 

154 

155 Returns 

156 ------- 

157 iterable : `DataCoordinateIterable` 

158 A `DataCoordinateIterable` with ``iterable.graph == graph``. 

159 May be ``self`` if ``graph == self.graph``. Elements are 

160 equivalent to those that would be created by calling 

161 `DataCoordinate.subset` on all elements in ``self``, possibly 

162 with deduplication and/or reordering (depending on the subclass, 

163 which may make more specific guarantees). 

164 """ 

165 raise NotImplementedError() 

166 

167 

168class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

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

170 

171 A `DataCoordinateIterable` implementation that adapts a single 

172 `DataCoordinate` instance. 

173 

174 This class should only be used directly by other code in the module in 

175 which it is defined; all other code should interact with it only through 

176 the `DataCoordinateIterable` interface. 

177 

178 Parameters 

179 ---------- 

180 dataId : `DataCoordinate` 

181 The data ID to adapt. 

182 """ 

183 

184 def __init__(self, dataId: DataCoordinate): 

185 self._dataId = dataId 

186 

187 __slots__ = ("_dataId",) 

188 

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

190 yield self._dataId 

191 

192 def __len__(self) -> int: 

193 return 1 

194 

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

196 if isinstance(key, DataCoordinate): 

197 return key == self._dataId 

198 else: 

199 return False 

200 

201 @property 

202 def graph(self) -> DimensionGraph: 

203 # Docstring inherited from DataCoordinateIterable. 

204 return self._dataId.graph 

205 

206 def hasFull(self) -> bool: 

207 # Docstring inherited from DataCoordinateIterable. 

208 return self._dataId.hasFull() 

209 

210 def hasRecords(self) -> bool: 

211 # Docstring inherited from DataCoordinateIterable. 

212 return self._dataId.hasRecords() 

213 

214 def subset(self, graph: DimensionGraph) -> _ScalarDataCoordinateIterable: 

215 # Docstring inherited from DataCoordinateIterable. 

216 return _ScalarDataCoordinateIterable(self._dataId.subset(graph)) 

217 

218 

219class _DataCoordinateCollectionBase(DataCoordinateIterable): 

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

221 

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

223 native Python collection. 

224 

225 This class is intended only to be used as an intermediate base class for 

226 `DataCoordinateIterables` that assume a more specific type of collection 

227 and can hence make more informed choices for how to implement some methods. 

228 

229 Parameters 

230 ---------- 

231 dataIds : `collections.abc.Collection` [ `DataCoordinate` ] 

232 A collection of `DataCoordinate` instances, with dimensions equal to 

233 ``graph``. 

234 graph : `DimensionGraph` 

235 Dimensions identified by all data IDs in the set. 

236 hasFull : `bool`, optional 

237 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

238 `True` for all given data IDs. If `False`, no such guarantee is made, 

239 and `hasFull` will always return `False`. If `None` (default), 

240 `hasFull` will be computed from the given data IDs, immediately if 

241 ``check`` is `True`, or on first use if ``check`` is `False`. 

242 hasRecords : `bool`, optional 

243 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

244 returns `True` for all given data IDs. If `False`, no such guarantee 

245 is made and `hasRecords` will always return `False`. If `None` 

246 (default), `hasRecords` will be computed from the given data IDs, 

247 immediately if ``check`` is `True`, or on first use if ``check`` is 

248 `False`. 

249 check: `bool`, optional 

250 If `True` (default) check that all data IDs are consistent with the 

251 given ``graph`` and state flags at construction. If `False`, no 

252 checking will occur. 

253 """ 

254 

255 def __init__( 

256 self, 

257 dataIds: Collection[DataCoordinate], 

258 graph: DimensionGraph, 

259 *, 

260 hasFull: bool | None = None, 

261 hasRecords: bool | None = None, 

262 check: bool = True, 

263 ): 

264 self._dataIds = dataIds 

265 self._graph = graph 

266 if check: 

267 for dataId in self._dataIds: 

268 if hasFull and not dataId.hasFull(): 

269 raise ValueError(f"{dataId} is not complete, but is required to be.") 

270 if hasRecords and not dataId.hasRecords(): 

271 raise ValueError(f"{dataId} has no records, but is required to.") 

272 if dataId.graph != self._graph: 

273 raise ValueError(f"Bad dimensions {dataId.graph}; expected {self._graph}.") 

274 if hasFull is None: 

275 hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

276 if hasRecords is None: 

277 hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

278 self._hasFull = hasFull 

279 self._hasRecords = hasRecords 

280 

281 __slots__ = ("_graph", "_dataIds", "_hasFull", "_hasRecords") 

282 

283 @property 

284 def graph(self) -> DimensionGraph: 

285 # Docstring inherited from DataCoordinateIterable. 

286 return self._graph 

287 

288 def hasFull(self) -> bool: 

289 # Docstring inherited from DataCoordinateIterable. 

290 if self._hasFull is None: 

291 self._hasFull = all(dataId.hasFull() for dataId in self._dataIds) 

292 return self._hasFull 

293 

294 def hasRecords(self) -> bool: 

295 # Docstring inherited from DataCoordinateIterable. 

296 if self._hasRecords is None: 

297 self._hasRecords = all(dataId.hasRecords() for dataId in self._dataIds) 

298 return self._hasRecords 

299 

300 def toSet(self) -> DataCoordinateSet: 

301 # Docstring inherited from DataCoordinateIterable. 

302 # Override base class to pass in attributes instead of results of 

303 # method calls for _hasFull and _hasRecords - those can be None, 

304 # and hence defer checking if that's what the user originally wanted. 

305 return DataCoordinateSet( 

306 frozenset(self._dataIds), 

307 graph=self._graph, 

308 hasFull=self._hasFull, 

309 hasRecords=self._hasRecords, 

310 check=False, 

311 ) 

312 

313 def toSequence(self) -> DataCoordinateSequence: 

314 # Docstring inherited from DataCoordinateIterable. 

315 # Override base class to pass in attributes instead of results of 

316 # method calls for _hasFull and _hasRecords - those can be None, 

317 # and hence defer checking if that's what the user originally wanted. 

318 return DataCoordinateSequence( 

319 tuple(self._dataIds), 

320 graph=self._graph, 

321 hasFull=self._hasFull, 

322 hasRecords=self._hasRecords, 

323 check=False, 

324 ) 

325 

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

327 return iter(self._dataIds) 

328 

329 def __len__(self) -> int: 

330 return len(self._dataIds) 

331 

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

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

334 return key in self._dataIds 

335 

336 def _subsetKwargs(self, graph: DimensionGraph) -> dict[str, Any]: 

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

338 

339 Parameters 

340 ---------- 

341 graph : `DimensionGraph` 

342 Dimensions passed to `subset`. 

343 

344 Returns 

345 ------- 

346 **kwargs 

347 A dict with `hasFull`, `hasRecords`, and `check` keys, associated 

348 with the appropriate values for a `subset` operation with the given 

349 dimensions. 

350 """ 

351 hasFull: bool | None 

352 if graph.dimensions <= self.graph.required: 

353 hasFull = True 

354 else: 

355 hasFull = self._hasFull 

356 return dict(hasFull=hasFull, hasRecords=self._hasRecords, check=False) 

357 

358 

359class DataCoordinateSet(_DataCoordinateCollectionBase): 

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

361 

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

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

364 

365 Parameters 

366 ---------- 

367 dataIds : `collections.abc.Set` [ `DataCoordinate` ] 

368 A set of `DataCoordinate` instances, with dimensions equal to 

369 ``graph``. If this is a mutable object, the caller must be able to 

370 guarantee that it will not be modified by any other holders. 

371 graph : `DimensionGraph` 

372 Dimensions identified by all data IDs in the set. 

373 hasFull : `bool`, optional 

374 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

375 `True` for all given data IDs. If `False`, no such guarantee is made, 

376 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

377 (default), `DataCoordinateSet.hasFull` will be computed from the given 

378 data IDs, immediately if ``check`` is `True`, or on first use if 

379 ``check`` is `False`. 

380 hasRecords : `bool`, optional 

381 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

382 returns `True` for all given data IDs. If `False`, no such guarantee 

383 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

384 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

385 from the given data IDs, immediately if ``check`` is `True`, or on 

386 first use if ``check`` is `False`. 

387 check: `bool`, optional 

388 If `True` (default) check that all data IDs are consistent with the 

389 given ``graph`` and state flags at construction. If `False`, no 

390 checking will occur. 

391 

392 Notes 

393 ----- 

394 `DataCoordinateSet` does not formally implement the `collections.abc.Set` 

395 interface, because that requires many binary operations to accept any 

396 set-like object as the other argument (regardless of what its elements 

397 might be), and it's much easier to ensure those operations never behave 

398 surprisingly if we restrict them to `DataCoordinateSet` or (sometimes) 

399 `DataCoordinateIterable`, and in most cases restrict that they identify 

400 the same dimensions. In particular: 

401 

402 - a `DataCoordinateSet` will compare as not equal to any object that is 

403 not a `DataCoordinateSet`, even native Python sets containing the exact 

404 same elements; 

405 

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

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

408 same dimensions (i.e. ``graph`` attribute); 

409 

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

411 be a `DataCoordinateIterable` with the same dimensions; 

412 

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

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

415 dimensions _and_ the same ``dtype``; 

416 

417 - named methods that create new sets (`intersection`, `union`, 

418 `symmetric_difference`, `difference`) require the other operand to be a 

419 `DataCoordinateIterable` with the same dimensions _and_ the same 

420 ``dtype``. 

421 

422 In addition, when the two operands differ in the return values of `hasFull` 

423 and/or `hasRecords`, we make no guarantees about what those methods will 

424 return on the new `DataCoordinateSet` (other than that they will accurately 

425 reflect what elements are in the new set - we just don't control which 

426 elements are contributed by each operand). 

427 """ 

428 

429 def __init__( 

430 self, 

431 dataIds: Set[DataCoordinate], 

432 graph: DimensionGraph, 

433 *, 

434 hasFull: bool | None = None, 

435 hasRecords: bool | None = None, 

436 check: bool = True, 

437 ): 

438 super().__init__(dataIds, graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

439 

440 _dataIds: Set[DataCoordinate] 

441 

442 __slots__ = () 

443 

444 def __str__(self) -> str: 

445 return str(set(self._dataIds)) 

446 

447 def __repr__(self) -> str: 

448 return ( 

449 f"DataCoordinateSet({set(self._dataIds)}, {self._graph!r}, " 

450 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

451 ) 

452 

453 def __eq__(self, other: Any) -> bool: 

454 if isinstance(other, DataCoordinateSet): 

455 return self._graph == other._graph and self._dataIds == other._dataIds 

456 return False 

457 

458 def __le__(self, other: DataCoordinateSet) -> bool: 

459 if self.graph != other.graph: 

460 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

461 return self._dataIds <= other._dataIds 

462 

463 def __ge__(self, other: DataCoordinateSet) -> bool: 

464 if self.graph != other.graph: 

465 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

466 return self._dataIds >= other._dataIds 

467 

468 def __lt__(self, other: DataCoordinateSet) -> bool: 

469 if self.graph != other.graph: 

470 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

471 return self._dataIds < other._dataIds 

472 

473 def __gt__(self, other: DataCoordinateSet) -> bool: 

474 if self.graph != other.graph: 

475 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

476 return self._dataIds > other._dataIds 

477 

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

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

480 

481 Parameters 

482 ---------- 

483 other : `DataCoordinateIterable` 

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

485 

486 Returns 

487 ------- 

488 issubset : `bool` 

489 `True` if all data IDs in ``self`` are also in ``other``, and 

490 `False` otherwise. 

491 """ 

492 if self.graph != other.graph: 

493 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

494 return self._dataIds <= other.toSet()._dataIds 

495 

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

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

498 

499 Parameters 

500 ---------- 

501 other : `DataCoordinateIterable` 

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

503 

504 Returns 

505 ------- 

506 issuperset : `bool` 

507 `True` if all data IDs in ``other`` are also in ``self``, and 

508 `False` otherwise. 

509 """ 

510 if self.graph != other.graph: 

511 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

512 return self._dataIds >= other.toSet()._dataIds 

513 

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

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

516 

517 Parameters 

518 ---------- 

519 other : `DataCoordinateIterable` 

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

521 

522 Returns 

523 ------- 

524 isdisjoint : `bool` 

525 `True` if there are no data IDs in both ``self`` and ``other``, and 

526 `False` otherwise. 

527 """ 

528 if self.graph != other.graph: 

529 raise ValueError(f"Inconsistent dimensions in set comparision: {self.graph} != {other.graph}.") 

530 return self._dataIds.isdisjoint(other.toSet()._dataIds) 

531 

532 def __and__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

533 if self.graph != other.graph: 

534 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

535 return DataCoordinateSet(self._dataIds & other._dataIds, self.graph, check=False) 

536 

537 def __or__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

538 if self.graph != other.graph: 

539 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

540 return DataCoordinateSet(self._dataIds | other._dataIds, self.graph, check=False) 

541 

542 def __xor__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

543 if self.graph != other.graph: 

544 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

545 return DataCoordinateSet(self._dataIds ^ other._dataIds, self.graph, check=False) 

546 

547 def __sub__(self, other: DataCoordinateSet) -> DataCoordinateSet: 

548 if self.graph != other.graph: 

549 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

550 return DataCoordinateSet(self._dataIds - other._dataIds, self.graph, check=False) 

551 

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

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

554 

555 Parameters 

556 ---------- 

557 other : `DataCoordinateIterable` 

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

559 

560 Returns 

561 ------- 

562 intersection : `DataCoordinateSet` 

563 A new `DataCoordinateSet` instance. 

564 """ 

565 if self.graph != other.graph: 

566 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

567 return DataCoordinateSet(self._dataIds & other.toSet()._dataIds, self.graph, check=False) 

568 

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

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

571 

572 Parameters 

573 ---------- 

574 other : `DataCoordinateIterable` 

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

576 

577 Returns 

578 ------- 

579 intersection : `DataCoordinateSet` 

580 A new `DataCoordinateSet` instance. 

581 """ 

582 if self.graph != other.graph: 

583 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

584 return DataCoordinateSet(self._dataIds | other.toSet()._dataIds, self.graph, check=False) 

585 

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

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

588 

589 Parameters 

590 ---------- 

591 other : `DataCoordinateIterable` 

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

593 

594 Returns 

595 ------- 

596 intersection : `DataCoordinateSet` 

597 A new `DataCoordinateSet` instance. 

598 """ 

599 if self.graph != other.graph: 

600 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

601 return DataCoordinateSet(self._dataIds ^ other.toSet()._dataIds, self.graph, check=False) 

602 

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

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

605 

606 Parameters 

607 ---------- 

608 other : `DataCoordinateIterable` 

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

610 

611 Returns 

612 ------- 

613 intersection : `DataCoordinateSet` 

614 A new `DataCoordinateSet` instance. 

615 """ 

616 if self.graph != other.graph: 

617 raise ValueError(f"Inconsistent dimensions in set operation: {self.graph} != {other.graph}.") 

618 return DataCoordinateSet(self._dataIds - other.toSet()._dataIds, self.graph, check=False) 

619 

620 def toSet(self) -> DataCoordinateSet: 

621 # Docstring inherited from DataCoordinateIterable. 

622 return self 

623 

624 def subset(self, graph: DimensionGraph) -> DataCoordinateSet: 

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

626 

627 Parameters 

628 ---------- 

629 graph : `DimensionGraph` 

630 Dimensions to be identified by the data IDs in the returned 

631 iterable. Must be a subset of ``self.graph``. 

632 

633 Returns 

634 ------- 

635 set : `DataCoordinateSet` 

636 A `DataCoordinateSet` with ``set.graph == graph``. 

637 Will be ``self`` if ``graph == self.graph``. Elements are 

638 equivalent to those that would be created by calling 

639 `DataCoordinate.subset` on all elements in ``self``, with 

640 deduplication but and in arbitrary order. 

641 """ 

642 if graph == self.graph: 

643 return self 

644 return DataCoordinateSet( 

645 {dataId.subset(graph) for dataId in self._dataIds}, graph, **self._subsetKwargs(graph) 

646 ) 

647 

648 

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

650 """Iterable supporting the full Sequence interface. 

651 

652 A `DataCoordinateIterable` implementation that supports the full 

653 `collections.abc.Sequence` interface. 

654 

655 Parameters 

656 ---------- 

657 dataIds : `collections.abc.Sequence` [ `DataCoordinate` ] 

658 A sequence of `DataCoordinate` instances, with dimensions equal to 

659 ``graph``. 

660 graph : `DimensionGraph` 

661 Dimensions identified by all data IDs in the set. 

662 hasFull : `bool`, optional 

663 If `True`, the caller guarantees that `DataCoordinate.hasFull` returns 

664 `True` for all given data IDs. If `False`, no such guarantee is made, 

665 and `DataCoordinateSet.hasFull` will always return `False`. If `None` 

666 (default), `DataCoordinateSet.hasFull` will be computed from the given 

667 data IDs, immediately if ``check`` is `True`, or on first use if 

668 ``check`` is `False`. 

669 hasRecords : `bool`, optional 

670 If `True`, the caller guarantees that `DataCoordinate.hasRecords` 

671 returns `True` for all given data IDs. If `False`, no such guarantee 

672 is made and `DataCoordinateSet.hasRecords` will always return `False`. 

673 If `None` (default), `DataCoordinateSet.hasRecords` will be computed 

674 from the given data IDs, immediately if ``check`` is `True`, or on 

675 first use if ``check`` is `False`. 

676 check: `bool`, optional 

677 If `True` (default) check that all data IDs are consistent with the 

678 given ``graph`` and state flags at construction. If `False`, no 

679 checking will occur. 

680 """ 

681 

682 def __init__( 

683 self, 

684 dataIds: Sequence[DataCoordinate], 

685 graph: DimensionGraph, 

686 *, 

687 hasFull: bool | None = None, 

688 hasRecords: bool | None = None, 

689 check: bool = True, 

690 ): 

691 super().__init__(tuple(dataIds), graph, hasFull=hasFull, hasRecords=hasRecords, check=check) 

692 

693 _dataIds: Sequence[DataCoordinate] 

694 

695 __slots__ = () 

696 

697 def __str__(self) -> str: 

698 return str(tuple(self._dataIds)) 

699 

700 def __repr__(self) -> str: 

701 return ( 

702 f"DataCoordinateSequence({tuple(self._dataIds)}, {self._graph!r}, " 

703 f"hasFull={self._hasFull}, hasRecords={self._hasRecords})" 

704 ) 

705 

706 def __eq__(self, other: Any) -> bool: 

707 if isinstance(other, DataCoordinateSequence): 

708 return self._graph == other._graph and self._dataIds == other._dataIds 

709 return False 

710 

711 @overload 

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

713 pass 

714 

715 @overload 

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

717 pass 

718 

719 def __getitem__(self, index: Any) -> Any: 

720 r = self._dataIds[index] 

721 if isinstance(index, slice): 

722 return DataCoordinateSequence( 

723 r, self._graph, hasFull=self._hasFull, hasRecords=self._hasRecords, check=False 

724 ) 

725 return r 

726 

727 def toSequence(self) -> DataCoordinateSequence: 

728 # Docstring inherited from DataCoordinateIterable. 

729 return self 

730 

731 def subset(self, graph: DimensionGraph) -> DataCoordinateSequence: 

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

733 

734 Parameters 

735 ---------- 

736 graph : `DimensionGraph` 

737 Dimensions to be identified by the data IDs in the returned 

738 iterable. Must be a subset of ``self.graph``. 

739 

740 Returns 

741 ------- 

742 set : `DataCoordinateSequence` 

743 A `DataCoordinateSequence` with ``set.graph == graph``. 

744 Will be ``self`` if ``graph == self.graph``. Elements are 

745 equivalent to those that would be created by calling 

746 `DataCoordinate.subset` on all elements in ``self``, in the same 

747 order and with no deduplication. 

748 """ 

749 if graph == self.graph: 

750 return self 

751 return DataCoordinateSequence( 

752 tuple(dataId.subset(graph) for dataId in self._dataIds), graph, **self._subsetKwargs(graph) 

753 )