Coverage for python / lsst / daf / butler / tests / registry_data / spatial.py: 0%

174 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:36 +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 

28"""A script that creates and displays test data for topological spatial 

29operations. 

30 

31The test data created by this script is intended to cover the complete set of 

32interesting topological relationships between visit, visit_detector_region, 

33tract, patch, and commonSkyPix with a relatively small number of actual 

34dimension records. We use a different sky pixelization for visit- and 

35skymap-based regions as an easy way to get different kinds of regions with 

36interesting relationships. 

37 

38The data created by this script must be imported after that in base.yaml 

39(which defines the instrument and detectors it assumes). It defines visits 

40that don't actually correspond to any exposures; those could be added in 

41another export file (along with the visit_definition records that relate them) 

42in the future. 

43""" 

44 

45from __future__ import annotations 

46 

47__all__ = [] 

48 

49import argparse 

50import os.path 

51from collections.abc import Callable, Iterable, Iterator 

52from typing import Any 

53 

54import numpy as np 

55import yaml 

56from astropy.time import Time 

57from astropy.wcs import WCS 

58from matplotlib import pyplot 

59 

60import lsst.daf.butler # register Time/YAML conversions. # noqa: F401 

61from lsst.sphgeom import ( 

62 ConvexPolygon, 

63 HealpixPixelization, 

64 HtmPixelization, 

65 LonLat, 

66 Mq3cPixelization, 

67 Pixelization, 

68 Q3cPixelization, 

69 RangeSet, 

70 UnitVector3d, 

71 Vector3d, 

72) 

73 

74# Pixelization for which one pixel defines the overall area of interest. 

75PARENT_PIX = Mq3cPixelization(6) 

76 

77# Pixelization used as the commonSkyPix in butler (needs to be kept consistent 

78# with the dimensions.yaml used in tests). 

79COMMON_PIX = HtmPixelization(7) 

80 

81# Pixelization used to define visit and visit-detector regions. 

82# Doesn't matter what this is, as long as its different from COMMON_PIX and 

83# PATCH_GRID_PIX so we get interesting overlaps. 

84DETECTOR_GRID_PIX = Mq3cPixelization(10) 

85 

86# Pixelization used to define tract and patch regions. 

87# Doesn't matter what this is, as long as its different from COMMON_PIX and 

88# DETECTOR_GRID_PIX so we get interesting overlaps. 

89PATCH_GRID_PIX = Q3cPixelization(9) 

90 

91# Name of the instrument; this matches the dimension record in base.yaml, 

92# which must be imported before the YAML file created by this script. 

93INSTRUMENT_NAME = "Cam1" 

94 

95# Name of the skymap. 

96SKYMAP_NAME = "SkyMap1" 

97 

98# Data used to define the visits and visit-detector regions. 

99# 

100# The write_yaml function adds fields common to all records and transforms 

101# skypix indices into actual regions. 

102# 

103# Edit this data structure and examine the resulting plots to add more test 

104# dimension records; running with ``--show-detector-grid`` may be helpful. 

105VISIT_DATA: dict[int, dict[str, Any]] = { 

106 1: { 

107 "physical_filter": "Cam1-G", 

108 "day_obs": 20210909, 

109 "exposure_time": 60.0, 

110 "target_name": "test_target", 

111 "observation_reason": "science", 

112 "science_program": "test_survey", 

113 "zenith_angle": 5.0, 

114 "datetime_begin": Time("2021-09-09T03:00:00", format="isot", scale="tai"), 

115 "datetime_end": Time("2021-09-09T03:01:00", format="isot", scale="tai"), 

116 "detector_regions": { 

117 1: [12058870, 12058871, 12058872, 12058873], 

118 2: [12058823, 12058824, 12058818, 12058829], 

119 3: [12058848, 12058849, 12058850, 12058851], 

120 4: [12058846], 

121 }, 

122 }, 

123 2: { 

124 "physical_filter": "Cam1-R1", 

125 "day_obs": 20210909, 

126 "exposure_time": 45.0, 

127 "target_name": "test_target", 

128 "observation_reason": "science", 

129 "science_program": "test_survey", 

130 "zenith_angle": 10.0, 

131 "datetime_begin": Time("2021-09-09T03:02:00", format="isot", scale="tai"), 

132 "datetime_end": Time("2021-09-09T03:03:00", format="isot", scale="tai"), 

133 "detector_regions": { 

134 1: [12058857, 12058854, 12058646, 12058649], 

135 2: [12058842, 12058841, 12058661, 12058662], 

136 3: [12058642, 12058653, 12058641, 12058654], 

137 4: [12058659], 

138 }, 

139 }, 

140} 

141 

142# Data used to define the tract and patch regions. 

143# 

144# The write_yaml function adds fields common to all records and transforms 

145# skypix indices into actual regions. 

146# 

147# Edit this data structure and examine the resulting plots to add more test 

148# dimension records; running with ``--show-patch-grid`` may be helpful. 

149TRACT_DATA: dict[int, dict[int, dict[str, Any]]] = { 

150 0: { 

151 0: {"cell_x": 0, "cell_y": 0, "region": 458787}, 

152 1: {"cell_x": 1, "cell_y": 0, "region": 458790}, 

153 2: {"cell_x": 0, "cell_y": 1, "region": 458785}, 

154 3: {"cell_x": 1, "cell_y": 1, "region": 458788}, 

155 4: {"cell_x": 0, "cell_y": 2, "region": 458763}, 

156 5: {"cell_x": 1, "cell_y": 2, "region": 458766}, 

157 }, 

158 1: { 

159 0: {"cell_x": 0, "cell_y": 0, "region": 458761}, 

160 1: {"cell_x": 1, "cell_y": 0, "region": 458764}, 

161 2: {"cell_x": 0, "cell_y": 1, "region": 458755}, 

162 3: {"cell_x": 1, "cell_y": 1, "region": 458758}, 

163 4: {"cell_x": 0, "cell_y": 2, "region": 458753}, 

164 5: {"cell_x": 1, "cell_y": 2, "region": 458756}, 

165 }, 

166} 

167 

168 

169def main() -> None: 

170 """Run script.""" 

171 parser = argparse.ArgumentParser(description="Create and examine spatial-topology registry test data.") 

172 default_filename = os.path.join(os.path.dirname(__file__), "spatial.yaml") 

173 parser.add_argument( 

174 "--filename", type=str, default=default_filename, help="Filename for YAML export file." 

175 ) 

176 parser.add_argument( 

177 "--show-detector-grid", 

178 action="store_true", 

179 default=False, 

180 help="Show the skypix grid used to define visit/detector regions.", 

181 ) 

182 parser.add_argument( 

183 "--show-patch-grid", 

184 action="store_true", 

185 default=False, 

186 help="Show the skypix grid used to define patch regions.", 

187 ) 

188 parser.add_argument( 

189 "--no-common-skypix-grid", 

190 dest="common_skypix_grid", 

191 action="store_false", 

192 default=True, 

193 help="Do not show the common skypix grid.", 

194 ) 

195 parser.add_argument( 

196 "--show-healpix-grid", 

197 type=int, 

198 default=[], 

199 help="Show a HEALPIX grid of this level.", 

200 action="append", 

201 ) 

202 parser.add_argument( 

203 "--no-plot", action="store_false", dest="make_plots", default=True, help="Do not plot the regions." 

204 ) 

205 parser.add_argument( 

206 "--no-write", 

207 action="store_false", 

208 dest="write_yaml", 

209 default=True, 

210 help="Do not write the YAML export file.", 

211 ) 

212 namespace = parser.parse_args() 

213 if namespace.make_plots: 

214 make_plots( 

215 detector_grid=namespace.show_detector_grid, 

216 patch_grid=namespace.show_patch_grid, 

217 healpix_grids=namespace.show_healpix_grid, 

218 common_skypix_grid=namespace.common_skypix_grid, 

219 ) 

220 if namespace.write_yaml: 

221 write_yaml(namespace.filename) 

222 

223 

224def make_plots( 

225 detector_grid: bool, patch_grid: bool, common_skypix_grid: bool = True, healpix_grids: Iterable[int] = () 

226) -> None: 

227 """Plot the regions of the dimension records defined by this script. 

228 

229 Parameters 

230 ---------- 

231 detector_grid : `bool` 

232 If `True`, show the skypix grid used to define visit and visit-detector 

233 regions. 

234 patch_grid : `bool` 

235 If `True`, show the skypix grid used to define tract and patch regions. 

236 common_skypix_grid : `bool`, optional 

237 If `True`, show the common skypix grid. 

238 healpix_grids : `~collections.abc.Iterable` [`int`], optional 

239 Levels of healpix grids to display. 

240 """ 

241 parent_index = PARENT_PIX.index(UnitVector3d(1, 0, 0)) 

242 parent_pixel = PARENT_PIX.pixel(parent_index) 

243 common_ranges = COMMON_PIX.envelope(parent_pixel) 

244 detector_grid_ranges = DETECTOR_GRID_PIX.interior(parent_pixel) 

245 patch_grid_ranges = PATCH_GRID_PIX.envelope(parent_pixel) 

246 wcs = make_tangent_wcs(parent_pixel.getCentroid()) 

247 labels_used = set() 

248 pyplot.figure(figsize=(16, 16)) 

249 pyplot.axis("off") 

250 if common_skypix_grid: 

251 plot_pixels( 

252 COMMON_PIX, 

253 wcs, 

254 flatten_ranges(common_ranges), 

255 polygons(facecolor="none", edgecolor="black", label="htm7"), 

256 index_labels(color="black", alpha=0.5), 

257 ) 

258 if detector_grid: 

259 plot_pixels( 

260 DETECTOR_GRID_PIX, 

261 wcs, 

262 flatten_ranges(detector_grid_ranges), 

263 polygons( 

264 edgecolor="black", 

265 linewidth=1, 

266 alpha=0.5, 

267 linestyle=":", 

268 facecolor="none", 

269 ), 

270 index_labels(color="black", alpha=0.5), 

271 ) 

272 if patch_grid: 

273 plot_pixels( 

274 PATCH_GRID_PIX, 

275 wcs, 

276 flatten_ranges(patch_grid_ranges), 

277 polygons( 

278 edgecolor="black", 

279 linewidth=1, 

280 alpha=0.5, 

281 linestyle=":", 

282 facecolor="none", 

283 ), 

284 index_labels(color="black", alpha=0.5), 

285 ) 

286 for healpix_level in healpix_grids: 

287 pixelization = HealpixPixelization(healpix_level) 

288 healpix_ranges = pixelization.envelope(parent_pixel) 

289 plot_pixels( 

290 pixelization, 

291 wcs, 

292 flatten_ranges(healpix_ranges), 

293 polygons( 

294 edgecolor="magenta", 

295 linewidth=1, 

296 alpha=0.5, 

297 linestyle=":", 

298 facecolor="none", 

299 label=f"healpix{healpix_level}", 

300 ), 

301 index_labels(color="magenta", alpha=0.5), 

302 ) 

303 colors = iter(["red", "blue", "cyan", "green"]) 

304 for (visit_id, visit_data), color in zip(VISIT_DATA.items(), colors, strict=False): 

305 for detector_id, pixel_indices in visit_data["detector_regions"].items(): 

306 label: str | None = f"visit={visit_id}" 

307 if label in labels_used: 

308 label = None 

309 else: 

310 labels_used.add(label) 

311 plot_hull( 

312 DETECTOR_GRID_PIX, 

313 wcs, 

314 pixel_indices, 

315 polygons( 

316 edgecolor="none", 

317 facecolor=color, 

318 alpha=0.25, 

319 label=label, 

320 ), 

321 labels( 

322 text=str(detector_id), 

323 color=color, 

324 ), 

325 ) 

326 for (tract_id, tract_data), color in zip(TRACT_DATA.items(), colors, strict=True): 

327 for patch_id, patch_data in tract_data.items(): 

328 label = f"tract={tract_id}" 

329 if label in labels_used: 

330 label = None 

331 else: 

332 labels_used.add(label) 

333 plot_pixels( 

334 PATCH_GRID_PIX, 

335 wcs, 

336 [patch_data["region"]], 

337 polygons( 

338 edgecolor=color, 

339 facecolor=color, 

340 alpha=0.25, 

341 label=label, 

342 ), 

343 labels( 

344 text=str(patch_id), 

345 color=color, 

346 ), 

347 ) 

348 parent_vertices = wcs.wcs_world2pix(np.array([lonlat_tuple(v) for v in parent_pixel.getVertices()]), 0) 

349 pyplot.xlim(parent_vertices[:, 0].min(), parent_vertices[:, 0].max()) 

350 pyplot.ylim(parent_vertices[:, 1].min(), parent_vertices[:, 1].max()) 

351 pyplot.legend() 

352 pyplot.show() 

353 

354 

355def write_yaml(filename: str) -> None: 

356 """Write the YAML export script with dimension record definitions. 

357 

358 Parameters 

359 ---------- 

360 filename : `str` 

361 Name of the file to write. 

362 

363 Notes 

364 ----- 

365 This creates a YAML export file that defines records for the following 

366 dimensions: 

367 

368 - visit_system 

369 - visit 

370 - visit_detector_region 

371 - skymap 

372 - tract 

373 - patch 

374 

375 """ 

376 day_obs_records = [{"instrument": INSTRUMENT_NAME, "id": 20210909}] 

377 visit_records = [] 

378 visit_detector_records = [] 

379 for visit_id, visit_data in VISIT_DATA.items(): 

380 visit_vertices = [] 

381 for detector_id, pixel_indices in visit_data["detector_regions"].items(): 

382 detector_vertices = [] 

383 for index in pixel_indices: 

384 polygon = DETECTOR_GRID_PIX.pixel(index) 

385 detector_vertices.extend(polygon.getVertices()) 

386 visit_vertices.extend(detector_vertices) 

387 visit_detector_records.append( 

388 { 

389 "instrument": INSTRUMENT_NAME, 

390 "visit": visit_id, 

391 "detector": detector_id, 

392 "region": ConvexPolygon(detector_vertices), 

393 } 

394 ) 

395 visit_record = visit_data.copy() 

396 del visit_record["detector_regions"] 

397 visit_record["instrument"] = INSTRUMENT_NAME 

398 visit_record["id"] = visit_id 

399 visit_record["name"] = str(visit_id) 

400 visit_record["region"] = ConvexPolygon(visit_vertices) 

401 visit_records.append(visit_record) 

402 skymap_records = [ 

403 { 

404 "name": SKYMAP_NAME, 

405 "hash": b"notreallyahashofanything!", 

406 "tract_max": 50, 

407 "patch_nx_max": 2, 

408 "patch_ny_max": 3, 

409 }, 

410 ] 

411 tract_records = [] 

412 patch_records = [] 

413 for tract_id, tract_data in TRACT_DATA.items(): 

414 tract_vertices = [] 

415 for patch_id, patch_data in tract_data.items(): 

416 patch_polygon = PATCH_GRID_PIX.pixel(patch_data["region"]) 

417 tract_vertices.extend(patch_polygon.getVertices()) 

418 patch_record = patch_data.copy() 

419 patch_record["region"] = patch_polygon 

420 patch_record["id"] = patch_id 

421 patch_record["tract"] = tract_id 

422 patch_record["skymap"] = SKYMAP_NAME 

423 patch_records.append(patch_record) 

424 tract_record: dict[str, Any] = {} 

425 tract_record["id"] = tract_id 

426 tract_record["skymap"] = SKYMAP_NAME 

427 tract_record["region"] = ConvexPolygon(tract_vertices) 

428 tract_records.append(tract_record) 

429 document = { 

430 "description": "Butler Data Repository Export", 

431 "version": "1.0.2", 

432 "universe_version": 7, 

433 "universe_namespace": "daf_butler", 

434 "data": [ 

435 { 

436 "type": "dimension", 

437 "element": "day_obs", 

438 "records": day_obs_records, 

439 }, 

440 { 

441 "type": "dimension", 

442 "element": "visit", 

443 "records": visit_records, 

444 }, 

445 { 

446 "type": "dimension", 

447 "element": "visit_detector_region", 

448 "records": visit_detector_records, 

449 }, 

450 { 

451 "type": "dimension", 

452 "element": "skymap", 

453 "records": skymap_records, 

454 }, 

455 { 

456 "type": "dimension", 

457 "element": "tract", 

458 "records": tract_records, 

459 }, 

460 { 

461 "type": "dimension", 

462 "element": "patch", 

463 "records": patch_records, 

464 }, 

465 ], 

466 } 

467 with open(filename, mode="w") as file: 

468 file.write("# Spatial test data; see spatial.py for more information.\n") 

469 yaml.dump(document, file, sort_keys=False) 

470 

471 

472def lonlat_tuple(position: LonLat | Vector3d) -> tuple[float, float]: 

473 """Transform a `lsst.sphgeom.LonLat` or `lsst.sphgeom.Vector3d` to a 

474 2-tuple of `float` degrees. 

475 """ 

476 lonlat = LonLat(position) 

477 return (lonlat.getLon().asDegrees(), lonlat.getLat().asDegrees()) 

478 

479 

480def make_tangent_wcs(position: LonLat | Vector3d) -> WCS: 

481 """Create an `astropy.WCS` that maps the sky to a tangent plane with 

482 degree-unit pixels at the given point. 

483 

484 Notes 

485 ----- 

486 This uses astropy instead of afw just to avoid a new dependency. I suspect 

487 that with a tiny bit of math I could just convert directly from 

488 `sphgeom.UnitVector3d` to a point in a Euclidean 2-d plane, which is all I 

489 want, but this seems fine as-is. 

490 """ 

491 result = WCS(naxis=2) 

492 result.wcs.crpix = [0.0, 0.0] 

493 result.wcs.crval = lonlat_tuple(position) 

494 result.wcs.ctype = ["RA---TAN", "DEC--TAN"] 

495 result.wcs.cd = [[1.0, 0.0], [0.0, 1.0]] 

496 return result 

497 

498 

499def project_polygon_center(wcs: WCS, polygon: ConvexPolygon) -> np.ndarray: 

500 """Return the WCS-projected center of the given polygon as a 2-element 

501 `float` array. 

502 """ 

503 return wcs.wcs_world2pix(np.array(lonlat_tuple(polygon.getCentroid()))[np.newaxis, :], 0)[0] 

504 

505 

506def project_polygon_vertices(wcs: WCS, polygon: ConvexPolygon) -> np.ndarray: 

507 """Return the WCS-projected vertices of the given polygon as a `float` 

508 array with shape ``(n, 2)``. 

509 """ 

510 vertices_sky = [] 

511 for vertex in polygon.getVertices(): 

512 vertices_sky.append(lonlat_tuple(vertex)) 

513 return wcs.wcs_world2pix(np.array(vertices_sky), 0) 

514 

515 

516def plot_pixels( 

517 pixelization: Pixelization, 

518 wcs: WCS, 

519 indices: Iterable[int], 

520 *callbacks: Callable[[int, np.ndarray, np.ndarray], None], 

521) -> None: 

522 """Perform plotting actions defined by callbacks on each of a series of 

523 skypix pixels. 

524 

525 Parameters 

526 ---------- 

527 pixelization : `lsst.sphgeom.Pixelization` 

528 Pixelization that interprets ``indices``. 

529 wcs : `WCS` 

530 Tangent plane to project spherical polygons onto. 

531 indices : `~collections.abc.Iterable` [ `int` ] 

532 Pixel indices to plot. 

533 *callbacks 

534 Callbacks to call for each pixel, passing the pixel index, the 

535 projected center, and the projected vertices. 

536 """ 

537 for index in indices: 

538 polygon = pixelization.pixel(index) 

539 center = project_polygon_center(wcs, polygon) 

540 vertices = project_polygon_vertices(wcs, polygon) 

541 for callback in callbacks: 

542 callback(index, center, vertices) 

543 

544 

545def plot_hull( 

546 pixelization: Pixelization, 

547 wcs: WCS, 

548 indices: Iterable[int], 

549 *callbacks: Callable[[list[int], np.ndarray, np.ndarray], None], 

550) -> None: 

551 """Perform plotting actions defined by callbacks on the convex hull of 

552 a series of skypix pixels. 

553 

554 Parameters 

555 ---------- 

556 pixelization : `lsst.sphgeom.Pixelization` 

557 Pixelization that interprets ``indices``. 

558 wcs : `WCS` 

559 Tangent plane to project spherical polygons onto. 

560 indices : `~collections.abc.Iterable` [ `int` ] 

561 Pixel indices to plot. 

562 *callbacks 

563 Callbacks to call passing the list of pixel indices, the 

564 projected center of the convex hull, and the projected vertices of the 

565 convex hull. 

566 """ 

567 vertices = [] 

568 indices = list(indices) 

569 for index in indices: 

570 polygon = pixelization.pixel(index) 

571 vertices.extend(polygon.getVertices()) 

572 polygon = ConvexPolygon(vertices) 

573 projected_center = project_polygon_center(wcs, polygon) 

574 projected_vertices = project_polygon_vertices(wcs, polygon) 

575 for callback in callbacks: 

576 callback(indices, projected_center, projected_vertices) 

577 

578 

579def polygons(label: str | None = None, **kwargs: Any) -> Callable[[Any, np.ndarray, np.ndarray], None]: 

580 """Return a callback for use with `plot_pixels` and `plot_hull` that plots 

581 polygon vertices. 

582 

583 Parameters 

584 ---------- 

585 label : `str`, optional 

586 Legend label for all polygons. Automatically deduplicated so the 

587 legend will only contain one entry for each call. 

588 **kwargs 

589 Forwarded to `matplotlib.pyplot.fill`. 

590 

591 Returns 

592 ------- 

593 func : `~collections.abc.Callable` 

594 Callable for use with `plot_hull` or `plot_pixels`. 

595 """ 

596 labels_used = set() 

597 

598 def func(index: Any, center: np.ndarray, vertices: np.ndarray) -> None: 

599 if label is not None and label in labels_used: 

600 label_to_use = None 

601 else: 

602 label_to_use = label 

603 labels_used.add(label) 

604 pyplot.fill( 

605 vertices[:, 0], 

606 vertices[:, 1], 

607 label=label_to_use, 

608 **kwargs, 

609 ) 

610 

611 return func 

612 

613 

614def index_labels(**kwargs: Any) -> Callable[[int, np.ndarray, np.ndarray], None]: 

615 """Return a callback for use with `plot_pixels` and `plot_hull` that adds 

616 text annotations with pixel indices at pixel centers. 

617 

618 Parameters 

619 ---------- 

620 **kwargs 

621 Forwarded to `matplotlib.pyplot.text`. 

622 """ 

623 

624 def func(index: int, center: np.ndarray, vertices: np.ndarray) -> None: 

625 pyplot.text( 

626 center[0], 

627 center[1], 

628 str(index), 

629 ha="center", 

630 va="center", 

631 **kwargs, 

632 ) 

633 

634 return func 

635 

636 

637def labels(text: str, **kwargs: Any) -> Callable[[Any, np.ndarray, np.ndarray], None]: 

638 """Return a callback for use with `plot_pixels` and `plot_hull` that adds 

639 text annotations with the given text. 

640 

641 Parameters 

642 ---------- 

643 text : `str` 

644 Label text. 

645 **kwargs 

646 Forwarded to `matplotlib.pyplot.text`. 

647 """ 

648 

649 def func(index: Any, center: np.ndarray, vertices: np.ndarray) -> None: 

650 pyplot.text(center[0], center[1], text, ha="center", va="center", **kwargs) 

651 

652 return func 

653 

654 

655def flatten_ranges(ranges: RangeSet) -> Iterator[int]: 

656 """Flatten an `lsst.sphgeom.RangeSet` into an iterator over pixel 

657 indices. 

658 """ 

659 for begin, end in ranges: 

660 yield from range(begin, end) 

661 

662 

663if __name__ == "__main__": 

664 main()