Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 typing import ( 

32 AbstractSet, 

33 Any, 

34 Callable, 

35 Collection, 

36 Dict, 

37 Iterable, 

38 Iterator, 

39 List, 

40 Optional, 

41 overload, 

42 Sequence, 

43) 

44 

45import sqlalchemy 

46 

47from ..simpleQuery import SimpleQuery 

48from .coordinate import DataCoordinate 

49from .graph import DimensionGraph 

50from .universe import DimensionUniverse 

51 

52 

53class DataCoordinateIterable(Iterable[DataCoordinate]): 

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

55 

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

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

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

59 """ 

60 

61 __slots__ = () 

62 

63 @staticmethod 

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

65 """Return a `DataCoordinateIterable` containing the single data ID 

66 given. 

67 

68 Parameters 

69 ---------- 

70 dataId : `DataCoordinate` 

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

72 an arbitrary mapping. No runtime checking is performed. 

73 

74 Returns 

75 ------- 

76 iterable : `DataCoordinateIterable` 

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

78 implementation-detail) subclass. Guaranteed to implement 

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

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

81 well as that of `DataCoordinateIterable`. 

82 """ 

83 return _ScalarDataCoordinateIterable(dataId) 

84 

85 @property 

86 @abstractmethod 

87 def graph(self) -> DimensionGraph: 

88 """The dimensions identified by these data IDs (`DimensionGraph`). 

89 """ 

90 raise NotImplementedError() 

91 

92 @property 

93 def universe(self) -> DimensionUniverse: 

94 """The universe that defines all known dimensions compatible with 

95 this iterable (`DimensionUniverse`). 

96 """ 

97 return self.graph.universe 

98 

99 @abstractmethod 

100 def hasFull(self) -> bool: 

101 """Return whether all data IDs in this iterable identify all 

102 dimensions, not just required dimensions. 

103 

104 Returns 

105 ------- 

106 state : `bool` 

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

108 If `False`, no guarantees are made. 

109 """ 

110 raise NotImplementedError() 

111 

112 @abstractmethod 

113 def hasRecords(self) -> bool: 

114 """Return whether all data IDs in this iterable contain 

115 `DimensionRecord` instances. 

116 

117 Returns 

118 ------- 

119 state : `bool` 

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

121 If `False`, no guarantees are made. 

122 """ 

123 raise NotImplementedError() 

124 

125 def toSet(self) -> DataCoordinateSet: 

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

127 

128 Returns 

129 ------- 

130 set : `DataCoordinateSet` 

131 A `DatasetCoordinateSet` instance with the same elements as 

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

133 already a `DataCoordinateSet`. 

134 """ 

135 return DataCoordinateSet(frozenset(self), graph=self.graph, 

136 hasFull=self.hasFull(), 

137 hasRecords=self.hasRecords(), 

138 check=False) 

139 

140 def toSequence(self) -> DataCoordinateSequence: 

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

142 

143 Returns 

144 ------- 

145 seq : `DataCoordinateSequence` 

146 A new `DatasetCoordinateSequence` with the same elements as 

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

148 `DataCoordinateSequence`. 

149 """ 

150 return DataCoordinateSequence(tuple(self), graph=self.graph, 

151 hasFull=self.hasFull(), 

152 hasRecords=self.hasRecords(), 

153 check=False) 

154 

155 def constrain(self, query: SimpleQuery, columns: Callable[[str], sqlalchemy.sql.ColumnElement]) -> None: 

156 """Constrain a SQL query to include or relate to only data IDs in 

157 this iterable. 

158 

159 Parameters 

160 ---------- 

161 query : `SimpleQuery` 

162 Struct that represents the SQL query to constrain, either by 

163 appending to its WHERE clause, joining a new table or subquery, 

164 or both. 

165 columns : `Callable` 

166 A callable that accepts `str` dimension names and returns 

167 SQLAlchemy objects representing a column for that dimension's 

168 primary key value in the query. 

169 """ 

170 toOrTogether: List[sqlalchemy.sql.ColumnElement] = [] 

171 for dataId in self: 

172 toOrTogether.append( 

173 sqlalchemy.sql.and_(*[ 

174 columns(dimension.name) == dataId[dimension.name] 

175 for dimension in self.graph.required 

176 ]) 

177 ) 

178 query.where.append(sqlalchemy.sql.or_(*toOrTogether)) 

179 

180 @abstractmethod 

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

182 """Return an iterable whose data IDs identify a subset of the 

183 dimensions that this one's do. 

184 

185 Parameters 

186 ---------- 

187 graph : `DimensionGraph` 

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

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

190 

191 Returns 

192 ------- 

193 iterable : `DataCoordinateIterable` 

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

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

196 equivalent to those that would be created by calling 

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

198 with deduplication and/or reordeding (depending on the subclass, 

199 which may make more specific guarantees). 

200 """ 

201 raise NotImplementedError() 

202 

203 

204class _ScalarDataCoordinateIterable(DataCoordinateIterable): 

205 """A `DataCoordinateIterable` implementation that adapts a single 

206 `DataCoordinate` instance. 

207 

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

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

210 the `DataCoordinateIterable` interface. 

211 

212 Parameters 

213 ---------- 

214 dataId : `DataCoordinate` 

215 The data ID to adapt. 

216 """ 

217 def __init__(self, dataId: DataCoordinate): 

218 self._dataId = dataId 

219 

220 __slots__ = ("_dataId",) 

221 

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

223 yield self._dataId 

224 

225 def __len__(self) -> int: 

226 return 1 

227 

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

229 if isinstance(key, DataCoordinate): 

230 return key == self._dataId 

231 else: 

232 return False 

233 

234 @property 

235 def graph(self) -> DimensionGraph: 

236 # Docstring inherited from DataCoordinateIterable. 

237 return self._dataId.graph 

238 

239 def hasFull(self) -> bool: 

240 # Docstring inherited from DataCoordinateIterable. 

241 return self._dataId.hasFull() 

242 

243 def hasRecords(self) -> bool: 

244 # Docstring inherited from DataCoordinateIterable. 

245 return self._dataId.hasRecords() 

246 

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

248 # Docstring inherited from DataCoordinateIterable. 

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

250 

251 

252class _DataCoordinateCollectionBase(DataCoordinateIterable): 

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

265 graph : `DimensionGraph` 

266 Dimensions identified by all data IDs in the set. 

267 hasFull : `bool`, optional 

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

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

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

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

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

273 hasRecords : `bool`, optional 

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

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

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

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

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

279 `False`. 

280 check: `bool`, optional 

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

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

283 checking will occur. 

284 """ 

285 def __init__(self, dataIds: Collection[DataCoordinate], graph: DimensionGraph, *, 

286 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None, 

287 check: bool = True): 

288 self._dataIds = dataIds 

289 self._graph = graph 

290 if check: 

291 for dataId in self._dataIds: 

292 if hasFull and not dataId.hasFull(): 

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

294 if hasRecords and not dataId.hasRecords(): 

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

296 if dataId.graph != self._graph: 

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

298 if hasFull is None: 

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

300 if hasRecords is None: 

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

302 self._hasFull = hasFull 

303 self._hasRecords = hasRecords 

304 

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

306 

307 @property 

308 def graph(self) -> DimensionGraph: 

309 # Docstring inherited from DataCoordinateIterable. 

310 return self._graph 

311 

312 def hasFull(self) -> bool: 

313 # Docstring inherited from DataCoordinateIterable. 

314 if self._hasFull is None: 

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

316 return self._hasFull 

317 

318 def hasRecords(self) -> bool: 

319 # Docstring inherited from DataCoordinateIterable. 

320 if self._hasRecords is None: 

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

322 return self._hasRecords 

323 

324 def toSet(self) -> DataCoordinateSet: 

325 # Docstring inherited from DataCoordinateIterable. 

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

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

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

329 return DataCoordinateSet(frozenset(self._dataIds), graph=self._graph, 

330 hasFull=self._hasFull, 

331 hasRecords=self._hasRecords, 

332 check=False) 

333 

334 def toSequence(self) -> DataCoordinateSequence: 

335 # Docstring inherited from DataCoordinateIterable. 

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

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

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

339 return DataCoordinateSequence(tuple(self._dataIds), graph=self._graph, 

340 hasFull=self._hasFull, 

341 hasRecords=self._hasRecords, 

342 check=False) 

343 

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

345 return iter(self._dataIds) 

346 

347 def __len__(self) -> int: 

348 return len(self._dataIds) 

349 

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

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

352 return key in self._dataIds 

353 

354 def _subsetKwargs(self, graph: DimensionGraph) -> Dict[str, Any]: 

355 """Return constructor keyword arguments useful for subclasses 

356 implementing `subset`. 

357 

358 Parameters 

359 ---------- 

360 graph : `DimensionGraph` 

361 Dimensions passed to `subset`. 

362 

363 Returns 

364 ------- 

365 kwargs : `dict` 

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

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

368 dimensions. 

369 """ 

370 hasFull: Optional[bool] 

371 if graph.dimensions.issubset(self.graph.required): 

372 hasFull = True 

373 else: 

374 hasFull = self._hasFull 

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

376 

377 

378class DataCoordinateSet(_DataCoordinateCollectionBase): 

379 """A `DataCoordinateIterable` implementation that adds some set-like 

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

381 

382 Parameters 

383 ---------- 

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

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

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

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

388 graph : `DimensionGraph` 

389 Dimensions identified by all data IDs in the set. 

390 hasFull : `bool`, optional 

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

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

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

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

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

396 ``check`` is `False`. 

397 hasRecords : `bool`, optional 

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

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

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

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

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

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

404 check: `bool`, optional 

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

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

407 checking will occur. 

408 

409 Notes 

410 ----- 

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

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

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

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

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

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

417 the same dimensions. In particular: 

418 

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

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

421 same elements; 

422 

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

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

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

426 

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

428 be a `DataCoordinateIterable` with the same dimensions; 

429 

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

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

432 dimensions _and_ the same ``dtype``; 

433 

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

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

436 `DataCoordinateIterable` with the same dimensions _and_ the same 

437 ``dtype``. 

438 

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

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

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

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

443 elements are contributed by each operand). 

444 """ 

445 def __init__(self, dataIds: AbstractSet[DataCoordinate], graph: DimensionGraph, *, 

446 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None, 

447 check: bool = True): 

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

449 

450 _dataIds: AbstractSet[DataCoordinate] 

451 

452 __slots__ = () 

453 

454 def __str__(self) -> str: 

455 return str(set(self._dataIds)) 

456 

457 def __repr__(self) -> str: 

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

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

460 

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

462 if isinstance(other, DataCoordinateSet): 

463 return ( 

464 self._graph == other._graph 

465 and self._dataIds == other._dataIds 

466 ) 

467 return False 

468 

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

470 if self.graph != other.graph: 

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

472 return self._dataIds <= other._dataIds 

473 

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

475 if self.graph != other.graph: 

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

477 return self._dataIds >= other._dataIds 

478 

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

480 if self.graph != other.graph: 

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

482 return self._dataIds < other._dataIds 

483 

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

485 if self.graph != other.graph: 

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

487 return self._dataIds > other._dataIds 

488 

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

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

491 

492 Parameters 

493 ---------- 

494 other : `DataCoordinateIterable` 

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

496 

497 Returns 

498 ------- 

499 issubset : `bool` 

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

501 `False` otherwise. 

502 """ 

503 if self.graph != other.graph: 

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

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

506 

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

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

509 

510 Parameters 

511 ---------- 

512 other : `DataCoordinateIterable` 

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

514 

515 Returns 

516 ------- 

517 issuperset : `bool` 

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

519 `False` otherwise. 

520 """ 

521 if self.graph != other.graph: 

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

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

524 

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

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

527 

528 Parameters 

529 ---------- 

530 other : `DataCoordinateIterable` 

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

532 

533 Returns 

534 ------- 

535 isdisjoint : `bool` 

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

537 `False` otherwise. 

538 """ 

539 if self.graph != other.graph: 

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

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

542 

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

544 if self.graph != other.graph: 

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

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

547 

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

549 if self.graph != other.graph: 

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

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

552 

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

554 if self.graph != other.graph: 

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

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

557 

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

559 if self.graph != other.graph: 

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

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

562 

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

564 """Return a new set that contains all data IDs in both ``self`` and 

565 ``other``. 

566 

567 Parameters 

568 ---------- 

569 other : `DataCoordinateIterable` 

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

571 

572 Returns 

573 ------- 

574 intersection : `DataCoordinateSet` 

575 A new `DataCoordinateSet` instance. 

576 """ 

577 if self.graph != other.graph: 

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

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

580 

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

582 """Return a new set that contains all data IDs in either ``self`` or 

583 ``other``. 

584 

585 Parameters 

586 ---------- 

587 other : `DataCoordinateIterable` 

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

589 

590 Returns 

591 ------- 

592 intersection : `DataCoordinateSet` 

593 A new `DataCoordinateSet` instance. 

594 """ 

595 if self.graph != other.graph: 

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

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

598 

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

600 """Return a new set that contains all data IDs in either ``self`` or 

601 ``other``, but not both. 

602 

603 Parameters 

604 ---------- 

605 other : `DataCoordinateIterable` 

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

607 

608 Returns 

609 ------- 

610 intersection : `DataCoordinateSet` 

611 A new `DataCoordinateSet` instance. 

612 """ 

613 if self.graph != other.graph: 

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

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

616 

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

618 """Return a new set that contains all data IDs in ``self`` that are not 

619 in ``other``. 

620 

621 Parameters 

622 ---------- 

623 other : `DataCoordinateIterable` 

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

625 

626 Returns 

627 ------- 

628 intersection : `DataCoordinateSet` 

629 A new `DataCoordinateSet` instance. 

630 """ 

631 if self.graph != other.graph: 

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

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

634 

635 def toSet(self) -> DataCoordinateSet: 

636 # Docstring inherited from DataCoordinateIterable. 

637 return self 

638 

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

640 """Return a set whose data IDs identify a subset of the 

641 dimensions that this one's do. 

642 

643 Parameters 

644 ---------- 

645 graph : `DimensionGraph` 

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

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

648 

649 Returns 

650 ------- 

651 set : `DataCoordinateSet` 

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

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

654 equivalent to those that would be created by calling 

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

656 deduplication but and in arbitrary order. 

657 """ 

658 if graph == self.graph: 

659 return self 

660 return DataCoordinateSet( 

661 {dataId.subset(graph) for dataId in self._dataIds}, 

662 graph, 

663 **self._subsetKwargs(graph) 

664 ) 

665 

666 

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

668 """A `DataCoordinateIterable` implementation that supports the full 

669 `collections.abc.Sequence` interface. 

670 

671 Parameters 

672 ---------- 

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

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

675 ``graph``. 

676 graph : `DimensionGraph` 

677 Dimensions identified by all data IDs in the set. 

678 hasFull : `bool`, optional 

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

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

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

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

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

684 ``check`` is `False`. 

685 hasRecords : `bool`, optional 

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

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

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

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

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

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

692 check: `bool`, optional 

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

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

695 checking will occur. 

696 """ 

697 def __init__(self, dataIds: Sequence[DataCoordinate], graph: DimensionGraph, *, 

698 hasFull: Optional[bool] = None, hasRecords: Optional[bool] = None, 

699 check: bool = True): 

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

701 

702 _dataIds: Sequence[DataCoordinate] 

703 

704 __slots__ = () 

705 

706 def __str__(self) -> str: 

707 return str(tuple(self._dataIds)) 

708 

709 def __repr__(self) -> str: 

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

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

712 

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

714 if isinstance(other, DataCoordinateSequence): 

715 return ( 

716 self._graph == other._graph 

717 and self._dataIds == other._dataIds 

718 ) 

719 return False 

720 

721 @overload 

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

723 pass 

724 

725 @overload # noqa: F811 

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

727 pass 

728 

729 def __getitem__(self, index: Any) -> Any: # noqa: F811 

730 r = self._dataIds[index] 

731 if isinstance(index, slice): 

732 return DataCoordinateSequence(r, self._graph, 

733 hasFull=self._hasFull, hasRecords=self._hasRecords, 

734 check=False) 

735 return r 

736 

737 def toSequence(self) -> DataCoordinateSequence: 

738 # Docstring inherited from DataCoordinateIterable. 

739 return self 

740 

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

742 """Return a sequence whose data IDs identify a subset of the 

743 dimensions that this one's do. 

744 

745 Parameters 

746 ---------- 

747 graph : `DimensionGraph` 

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

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

750 

751 Returns 

752 ------- 

753 set : `DataCoordinateSequence` 

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

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

756 equivalent to those that would be created by calling 

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

758 order and with no deduplication. 

759 """ 

760 if graph == self.graph: 

761 return self 

762 return DataCoordinateSequence( 

763 tuple(dataId.subset(graph) for dataId in self._dataIds), 

764 graph, 

765 **self._subsetKwargs(graph) 

766 )