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 

67 Parameters 

68 ---------- 

69 dataId : `DataCoordinate` 

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

71 an arbitrary mapping. No runtime checking is performed. 

72 

73 Returns 

74 ------- 

75 iterable : `DataCoordinateIterable` 

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

77 implementation-detail) subclass. Guaranteed to implement 

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

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

80 well as that of `DataCoordinateIterable`. 

81 """ 

82 return _ScalarDataCoordinateIterable(dataId) 

83 

84 @property 

85 @abstractmethod 

86 def graph(self) -> DimensionGraph: 

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

88 raise NotImplementedError() 

89 

90 @property 

91 def universe(self) -> DimensionUniverse: 

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

93 

94 (`DimensionUniverse`). 

95 """ 

96 return self.graph.universe 

97 

98 @abstractmethod 

99 def hasFull(self) -> bool: 

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

101 

102 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 records. 

115 

116 Returns 

117 ------- 

118 state : `bool` 

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

120 If `False`, no guarantees are made. 

121 """ 

122 raise NotImplementedError() 

123 

124 def toSet(self) -> DataCoordinateSet: 

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

126 

127 Returns 

128 ------- 

129 set : `DataCoordinateSet` 

130 A `DatasetCoordinateSet` instance with the same elements as 

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

132 already a `DataCoordinateSet`. 

133 """ 

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

135 hasFull=self.hasFull(), 

136 hasRecords=self.hasRecords(), 

137 check=False) 

138 

139 def toSequence(self) -> DataCoordinateSequence: 

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

141 

142 Returns 

143 ------- 

144 seq : `DataCoordinateSequence` 

145 A new `DatasetCoordinateSequence` with the same elements as 

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

147 `DataCoordinateSequence`. 

148 """ 

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

150 hasFull=self.hasFull(), 

151 hasRecords=self.hasRecords(), 

152 check=False) 

153 

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

155 """Constrain a SQL query to include or relate to only known data IDs. 

156 

157 Parameters 

158 ---------- 

159 query : `SimpleQuery` 

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

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

162 or both. 

163 columns : `Callable` 

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

165 SQLAlchemy objects representing a column for that dimension's 

166 primary key value in the query. 

167 """ 

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

169 for dataId in self: 

170 toOrTogether.append( 

171 sqlalchemy.sql.and_(*[ 

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

173 for dimension in self.graph.required 

174 ]) 

175 ) 

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

177 

178 @abstractmethod 

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

180 """Return a subset iterable. 

181 

182 This subset iterable returns data IDs that 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 """An iterable for a single `DataCoordinate`. 

206 

207 A `DataCoordinateIterable` implementation that adapts a single 

208 `DataCoordinate` instance. 

209 

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

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

212 the `DataCoordinateIterable` interface. 

213 

214 Parameters 

215 ---------- 

216 dataId : `DataCoordinate` 

217 The data ID to adapt. 

218 """ 

219 

220 def __init__(self, dataId: DataCoordinate): 

221 self._dataId = dataId 

222 

223 __slots__ = ("_dataId",) 

224 

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

226 yield self._dataId 

227 

228 def __len__(self) -> int: 

229 return 1 

230 

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

232 if isinstance(key, DataCoordinate): 

233 return key == self._dataId 

234 else: 

235 return False 

236 

237 @property 

238 def graph(self) -> DimensionGraph: 

239 # Docstring inherited from DataCoordinateIterable. 

240 return self._dataId.graph 

241 

242 def hasFull(self) -> bool: 

243 # Docstring inherited from DataCoordinateIterable. 

244 return self._dataId.hasFull() 

245 

246 def hasRecords(self) -> bool: 

247 # Docstring inherited from DataCoordinateIterable. 

248 return self._dataId.hasRecords() 

249 

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

251 # Docstring inherited from DataCoordinateIterable. 

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

253 

254 

255class _DataCoordinateCollectionBase(DataCoordinateIterable): 

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

257 

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

259 native Python collection. 

260 

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

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

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

264 

265 Parameters 

266 ---------- 

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

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

269 ``graph``. 

270 graph : `DimensionGraph` 

271 Dimensions identified by all data IDs in the set. 

272 hasFull : `bool`, optional 

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

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

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

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

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

278 hasRecords : `bool`, optional 

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

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

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

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

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

284 `False`. 

285 check: `bool`, optional 

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

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

288 checking will occur. 

289 """ 

290 

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

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

293 check: bool = True): 

294 self._dataIds = dataIds 

295 self._graph = graph 

296 if check: 

297 for dataId in self._dataIds: 

298 if hasFull and not dataId.hasFull(): 

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

300 if hasRecords and not dataId.hasRecords(): 

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

302 if dataId.graph != self._graph: 

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

304 if hasFull is None: 

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

306 if hasRecords is None: 

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

308 self._hasFull = hasFull 

309 self._hasRecords = hasRecords 

310 

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

312 

313 @property 

314 def graph(self) -> DimensionGraph: 

315 # Docstring inherited from DataCoordinateIterable. 

316 return self._graph 

317 

318 def hasFull(self) -> bool: 

319 # Docstring inherited from DataCoordinateIterable. 

320 if self._hasFull is None: 

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

322 return self._hasFull 

323 

324 def hasRecords(self) -> bool: 

325 # Docstring inherited from DataCoordinateIterable. 

326 if self._hasRecords is None: 

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

328 return self._hasRecords 

329 

330 def toSet(self) -> DataCoordinateSet: 

331 # Docstring inherited from DataCoordinateIterable. 

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

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

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

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

336 hasFull=self._hasFull, 

337 hasRecords=self._hasRecords, 

338 check=False) 

339 

340 def toSequence(self) -> DataCoordinateSequence: 

341 # Docstring inherited from DataCoordinateIterable. 

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

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

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

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

346 hasFull=self._hasFull, 

347 hasRecords=self._hasRecords, 

348 check=False) 

349 

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

351 return iter(self._dataIds) 

352 

353 def __len__(self) -> int: 

354 return len(self._dataIds) 

355 

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

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

358 return key in self._dataIds 

359 

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

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

362 

363 Parameters 

364 ---------- 

365 graph : `DimensionGraph` 

366 Dimensions passed to `subset`. 

367 

368 Returns 

369 ------- 

370 kwargs : `dict` 

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

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

373 dimensions. 

374 """ 

375 hasFull: Optional[bool] 

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

377 hasFull = True 

378 else: 

379 hasFull = self._hasFull 

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

381 

382 

383class DataCoordinateSet(_DataCoordinateCollectionBase): 

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

385 

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

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

388 

389 Parameters 

390 ---------- 

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

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

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

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

395 graph : `DimensionGraph` 

396 Dimensions identified by all data IDs in the set. 

397 hasFull : `bool`, optional 

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

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

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

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

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

403 ``check`` is `False`. 

404 hasRecords : `bool`, optional 

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

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

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

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

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

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

411 check: `bool`, optional 

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

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

414 checking will occur. 

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. ``graph`` 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__(self, dataIds: AbstractSet[DataCoordinate], graph: DimensionGraph, *, 

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

455 check: bool = True): 

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

457 

458 _dataIds: AbstractSet[DataCoordinate] 

459 

460 __slots__ = () 

461 

462 def __str__(self) -> str: 

463 return str(set(self._dataIds)) 

464 

465 def __repr__(self) -> str: 

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

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

468 

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

470 if isinstance(other, DataCoordinateSet): 

471 return ( 

472 self._graph == other._graph 

473 and self._dataIds == other._dataIds 

474 ) 

475 return False 

476 

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

478 if self.graph != other.graph: 

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

480 return self._dataIds <= other._dataIds 

481 

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

483 if self.graph != other.graph: 

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

485 return self._dataIds >= other._dataIds 

486 

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

488 if self.graph != other.graph: 

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

490 return self._dataIds < other._dataIds 

491 

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

493 if self.graph != other.graph: 

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

495 return self._dataIds > other._dataIds 

496 

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

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

499 

500 Parameters 

501 ---------- 

502 other : `DataCoordinateIterable` 

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

504 

505 Returns 

506 ------- 

507 issubset : `bool` 

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

509 `False` otherwise. 

510 """ 

511 if self.graph != other.graph: 

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

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

514 

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

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

517 

518 Parameters 

519 ---------- 

520 other : `DataCoordinateIterable` 

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

522 

523 Returns 

524 ------- 

525 issuperset : `bool` 

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

527 `False` otherwise. 

528 """ 

529 if self.graph != other.graph: 

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

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

532 

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

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

535 

536 Parameters 

537 ---------- 

538 other : `DataCoordinateIterable` 

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

540 

541 Returns 

542 ------- 

543 isdisjoint : `bool` 

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

545 `False` otherwise. 

546 """ 

547 if self.graph != other.graph: 

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

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

550 

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

552 if self.graph != other.graph: 

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

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

555 

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

557 if self.graph != other.graph: 

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

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

560 

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

562 if self.graph != other.graph: 

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

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

565 

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

567 if self.graph != other.graph: 

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

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

570 

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

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

573 

574 Parameters 

575 ---------- 

576 other : `DataCoordinateIterable` 

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

578 

579 Returns 

580 ------- 

581 intersection : `DataCoordinateSet` 

582 A new `DataCoordinateSet` instance. 

583 """ 

584 if self.graph != other.graph: 

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

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

587 

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

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

590 

591 Parameters 

592 ---------- 

593 other : `DataCoordinateIterable` 

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

595 

596 Returns 

597 ------- 

598 intersection : `DataCoordinateSet` 

599 A new `DataCoordinateSet` instance. 

600 """ 

601 if self.graph != other.graph: 

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

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

604 

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

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

607 

608 Parameters 

609 ---------- 

610 other : `DataCoordinateIterable` 

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

612 

613 Returns 

614 ------- 

615 intersection : `DataCoordinateSet` 

616 A new `DataCoordinateSet` instance. 

617 """ 

618 if self.graph != other.graph: 

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

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

621 

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

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

624 

625 Parameters 

626 ---------- 

627 other : `DataCoordinateIterable` 

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

629 

630 Returns 

631 ------- 

632 intersection : `DataCoordinateSet` 

633 A new `DataCoordinateSet` instance. 

634 """ 

635 if self.graph != other.graph: 

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

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

638 

639 def toSet(self) -> DataCoordinateSet: 

640 # Docstring inherited from DataCoordinateIterable. 

641 return self 

642 

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

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

645 

646 Parameters 

647 ---------- 

648 graph : `DimensionGraph` 

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

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

651 

652 Returns 

653 ------- 

654 set : `DataCoordinateSet` 

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

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

657 equivalent to those that would be created by calling 

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

659 deduplication but and in arbitrary order. 

660 """ 

661 if graph == self.graph: 

662 return self 

663 return DataCoordinateSet( 

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

665 graph, 

666 **self._subsetKwargs(graph) 

667 ) 

668 

669 

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

671 """Iterable supporting the full Sequence interface. 

672 

673 A `DataCoordinateIterable` implementation that supports the full 

674 `collections.abc.Sequence` interface. 

675 

676 Parameters 

677 ---------- 

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

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

680 ``graph``. 

681 graph : `DimensionGraph` 

682 Dimensions identified by all data IDs in the set. 

683 hasFull : `bool`, optional 

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

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

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

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

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

689 ``check`` is `False`. 

690 hasRecords : `bool`, optional 

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

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

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

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

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

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

697 check: `bool`, optional 

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

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

700 checking will occur. 

701 """ 

702 

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

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

705 check: bool = True): 

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

707 

708 _dataIds: Sequence[DataCoordinate] 

709 

710 __slots__ = () 

711 

712 def __str__(self) -> str: 

713 return str(tuple(self._dataIds)) 

714 

715 def __repr__(self) -> str: 

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

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

718 

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

720 if isinstance(other, DataCoordinateSequence): 

721 return ( 

722 self._graph == other._graph 

723 and self._dataIds == other._dataIds 

724 ) 

725 return False 

726 

727 @overload 

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

729 pass 

730 

731 @overload # noqa: F811 (FIXME: remove for py 3.8+) 

732 def __getitem__(self, index: slice) -> DataCoordinateSequence: # noqa: F811 

733 pass 

734 

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

736 r = self._dataIds[index] 

737 if isinstance(index, slice): 

738 return DataCoordinateSequence(r, self._graph, 

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

740 check=False) 

741 return r 

742 

743 def toSequence(self) -> DataCoordinateSequence: 

744 # Docstring inherited from DataCoordinateIterable. 

745 return self 

746 

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

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

749 

750 Parameters 

751 ---------- 

752 graph : `DimensionGraph` 

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

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

755 

756 Returns 

757 ------- 

758 set : `DataCoordinateSequence` 

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

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

761 equivalent to those that would be created by calling 

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

763 order and with no deduplication. 

764 """ 

765 if graph == self.graph: 

766 return self 

767 return DataCoordinateSequence( 

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

769 graph, 

770 **self._subsetKwargs(graph) 

771 )