Coverage for python/lsst/images/cells/_provenance.py: 68%

194 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 08:38 +0000

1# This file is part of lsst-images. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ("CoaddProvenance", "CoaddProvenanceSerializationModel") 

15 

16from collections.abc import Iterable 

17from typing import TYPE_CHECKING, Any, ClassVar 

18 

19import astropy.table 

20import astropy.units as u 

21import numpy as np 

22import pydantic 

23 

24from .._cell_grid import CellGridBounds, CellIJ 

25from .._polygon import Polygon 

26from ..describe import DescribableMixin, DescribeOptions, FieldRole, Report, ReportField 

27from ..serialization import ArchiveTree, InputArchive, InvalidParameterError, OutputArchive, TableModel 

28 

29if TYPE_CHECKING: 

30 try: 

31 from lsst.afw.geom import Polygon as LegacyPolygon 

32 from lsst.cell_coadds import CoaddInputs as LegacyCellCoaddInputs 

33 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd 

34 from lsst.cell_coadds import ObservationIdentifiers as LegacyObservationIdentifiers 

35 from lsst.skymap import Index2D as LegacyIndex2D 

36 except ImportError: 

37 type LegacyIndex2D = Any # type: ignore[no-redef] 

38 type LegacyCellCoaddInputs = Any # type: ignore[no-redef] 

39 type LegacyPolygon = Any # type: ignore[no-redef] 

40 type LegacyMultipleCellCoadd = Any # type: ignore[no-redef] 

41 type LegacyObservationIdentifiers = Any # type: ignore[no-redef] 

42 

43 

44class CoaddProvenance(DescribableMixin): 

45 """A pair of tables that record the inputs to a cell-based coadd. 

46 

47 Parameters 

48 ---------- 

49 inputs 

50 A table of {visit, detector} combinations that contribute to any cell 

51 in the coadd. 

52 contributions 

53 A table of {visit, detector, cell} combinations that describe how an 

54 observation contributed to a cell. 

55 

56 Notes 

57 ----- 

58 This object can represent the provenance of a whole patch, a single cell, 

59 or anything in between. In the single-cell case, the ``inputs`` and 

60 ``contributions`` tables have the same number of rows (but may not be 

61 ordered the same way!). 

62 """ 

63 

64 def __init__(self, inputs: astropy.table.Table, contributions: astropy.table.Table) -> None: 

65 self._inputs = inputs 

66 self._contributions = contributions 

67 

68 _INPUT_TABLE_COLUMNS: ClassVar[list[tuple[str, type, str]]] = [ 

69 ("instrument", np.object_, "Name of the instrument."), 

70 ("visit", np.uint64, "ID of the visit."), 

71 ("detector", np.uint16, "ID of the detector."), 

72 ("physical_filter", np.object_, "Full name of the bandpass filter."), 

73 ("day_obs", np.uint32, "Observation night as a YYYYMMDD integer."), 

74 ( 

75 "polygon", 

76 np.object_, 

77 ( 

78 "Polygon that approximates the overlap of the observation and the coadd patch, " 

79 "in coadd coordinates." 

80 ), 

81 ), 

82 ] 

83 

84 _CONTRIBUTION_TABLE_COLUMNS: ClassVar[list[tuple[str, type, str, u.UnitBase | None]]] = [ 

85 ("cell_i", np.uint16, "Y-axis index of the cell within the patch.", None), 

86 ("cell_j", np.uint16, "X-axis index of the cell within the patch.", None), 

87 ("instrument", np.object_, "Name of the instrument.", None), 

88 ("visit", np.uint64, "ID of the visit.", None), 

89 ("detector", np.uint16, "ID of the detector.", None), 

90 ("overlaps_center", np.bool_, "Whether a this observation overlaps the center of the cell.", None), 

91 ("overlap_fraction", np.float64, "Fraction of the cell that is covered by the overlap region.", None), 

92 ("unmasked_fraction", np.float64, "Fraction of the cell propagated to the coadd.", None), 

93 ("weight", np.float64, "Weight to be used for this input in this cell.", None), 

94 ("psf_shape_xx", np.float64, "Second order moments of the PSF.", u.pix**2), 

95 ("psf_shape_yy", np.float64, "Second order moments of the PSF.", u.pix**2), 

96 ("psf_shape_xy", np.float64, "Second order moments of the PSF.", u.pix**2), 

97 ( 

98 "psf_shape_flag", 

99 np.bool_, 

100 "Flag indicating whether the PSF shape measurement was successful.", 

101 None, 

102 ), 

103 ] 

104 

105 @classmethod 

106 def make_empty_input_table(cls, n_rows: int) -> astropy.table.Table: 

107 """Make an empty `inputs` table with a set number of rows. 

108 

109 Parameters 

110 ---------- 

111 n_rows 

112 Number of rows in the new table. 

113 """ 

114 return astropy.table.Table( 

115 [ 

116 astropy.table.Column(name=name, length=n_rows, dtype=dtype, description=description) 

117 for name, dtype, description in cls._INPUT_TABLE_COLUMNS 

118 ] 

119 ) 

120 

121 @classmethod 

122 def make_empty_contribution_table(cls, n_rows: int) -> astropy.table.Table: 

123 """Make an empty `contributions` table with a set number of rows. 

124 

125 Parameters 

126 ---------- 

127 n_rows 

128 Number of rows in the new table. 

129 """ 

130 return astropy.table.Table( 

131 [ 

132 astropy.table.Column( 

133 name=name, length=n_rows, dtype=dtype, description=description, unit=unit 

134 ) 

135 for name, dtype, description, unit in cls._CONTRIBUTION_TABLE_COLUMNS 

136 ] 

137 ) 

138 

139 @property 

140 def inputs(self) -> astropy.table.Table: 

141 """A table of {visit, detector} combinations that contribute to any 

142 cell in the coadd. 

143 """ 

144 return self._inputs 

145 

146 @property 

147 def contributions(self) -> astropy.table.Table: 

148 """A table of {visit, detector, cell} combinations that describe how an 

149 observation contributed to a cell. 

150 """ 

151 return self._contributions 

152 

153 def __getitem__(self, cell: CellIJ) -> CoaddProvenance: 

154 return self.subset([cell]) 

155 

156 def subset(self, cells: Iterable[CellIJ]) -> CoaddProvenance: 

157 """Return a new provenance object with just the given cells. 

158 

159 Parameters 

160 ---------- 

161 cells 

162 Cells to keep in the returned provenance. 

163 """ 

164 cells_to_keep = astropy.table.Table( 

165 rows=[(index.i, index.j) for index in cells], 

166 names=["cell_i", "cell_j"], 

167 dtype=[np.uint16, np.uint16], 

168 ) 

169 contributions = astropy.table.join(self._contributions, cells_to_keep) 

170 assert contributions.columns.keys() == {name for name, _, _, _ in self._CONTRIBUTION_TABLE_COLUMNS} 

171 inputs = astropy.table.join(contributions["instrument", "visit", "detector"], self._inputs) 

172 assert inputs.columns.keys() == {name for name, _, _ in self._INPUT_TABLE_COLUMNS} 

173 return CoaddProvenance(inputs=inputs, contributions=contributions) 

174 

175 def _describe( 

176 self, 

177 options: DescribeOptions = DescribeOptions(), 

178 /, 

179 *, 

180 bounds: CellGridBounds | None = None, 

181 ) -> Report: 

182 """Return a `Report` describing this provenance. 

183 

184 Parameters 

185 ---------- 

186 options : `DescribeOptions`, optional 

187 Rendering options. `DescribeOptions.brief` reports only the 

188 number of input images, which no column scan is needed to count. 

189 bounds : `CellGridBounds`, optional 

190 Cells the image this provenance belongs to has data for. When 

191 given, the number of cells with contributions is reported as a 

192 fraction of them. 

193 

194 Notes 

195 ----- 

196 The report summarizes the tables rather than rendering them. Rich 

197 renders an embedded `astropy.table.Table` as plain text and never 

198 consults the table's own ``_repr_html_``, so `inputs` and 

199 `contributions` describe themselves better than this report could, 

200 and a patch has tens of thousands of contribution rows. 

201 """ 

202 n_inputs = len(self._inputs) 

203 summary = ( 

204 f"CoaddProvenance({n_inputs} input image{'s' if n_inputs != 1 else ''})" 

205 if n_inputs 

206 else "CoaddProvenance(no input images)" 

207 ) 

208 if options.brief: 

209 return Report(type_name="CoaddProvenance", summary=summary) 

210 fields: list[ReportField] = [] 

211 if n_inputs: 

212 for column in ("instrument", "physical_filter"): 

213 fields.append( 

214 ReportField( 

215 label=column, 

216 value=", ".join(sorted({str(value) for value in self._inputs[column]})), 

217 role=FieldRole.DERIVED, 

218 ) 

219 ) 

220 n_visits = len(np.unique(self._inputs["visit"])) 

221 input_images = f"{n_inputs} from {n_visits} visit{'s' if n_visits != 1 else ''}" 

222 else: 

223 input_images = "none" 

224 fields.append(ReportField(label="input images", value=input_images, role=FieldRole.DERIVED)) 

225 if n_inputs: 

226 first_night = int(self._inputs["day_obs"].min()) 

227 last_night = int(self._inputs["day_obs"].max()) 

228 fields.append( 

229 ReportField( 

230 label="day_obs", 

231 value=str(first_night) if first_night == last_night else f"{first_night} - {last_night}", 

232 role=FieldRole.DERIVED, 

233 ) 

234 ) 

235 _, counts = np.unique( 

236 np.column_stack([self._contributions["cell_i"], self._contributions["cell_j"]]), 

237 axis=0, 

238 return_counts=True, 

239 ) 

240 if bounds is not None: 

241 n_cells_with_data = bounds.subgrid_size.i * bounds.subgrid_size.j - len(bounds.missing) 

242 cells = f"{len(counts)} of {n_cells_with_data} with contributions" 

243 else: 

244 cells = f"{len(counts)} with contributions" if len(counts) else "none" 

245 fields.append(ReportField(label="cells", value=cells, role=FieldRole.DERIVED)) 

246 if len(counts): 

247 low = int(counts.min()) 

248 high = int(counts.max()) 

249 per_cell = ( 

250 f"{low} input image{'s' if low != 1 else ''}" 

251 if low == high 

252 else f"{low} - {high} input images (median {np.median(counts):g})" 

253 ) 

254 fields.append(ReportField(label="per cell", value=per_cell, role=FieldRole.DERIVED)) 

255 return Report(type_name="CoaddProvenance", summary=summary, fields=fields) 

256 

257 def serialize(self, archive: OutputArchive[Any]) -> CoaddProvenanceSerializationModel: 

258 """Serialize the provenance to an output archive. 

259 

260 Parameters 

261 ---------- 

262 archive 

263 Archive to write to. 

264 """ 

265 inputs = self._inputs.copy(copy_data=False) 

266 contributions = self._contributions.copy(copy_data=False) 

267 instrument = CoaddProvenanceSerializationModel._fix_str_for_serialization( 

268 "instrument", inputs, contributions 

269 ) 

270 physical_filter = CoaddProvenanceSerializationModel._fix_str_for_serialization( 

271 "physical_filter", inputs 

272 ) 

273 CoaddProvenanceSerializationModel._fix_polygon_for_serialization(inputs) 

274 inputs_model = archive.add_table(inputs, name="inputs") 

275 contributions_model = archive.add_table(contributions, name="contributions") 

276 return CoaddProvenanceSerializationModel( 

277 instrument=instrument, 

278 physical_filter=physical_filter, 

279 inputs=inputs_model, 

280 contributions=contributions_model, 

281 ) 

282 

283 @staticmethod 

284 def from_legacy(legacy_cell_coadd: LegacyMultipleCellCoadd) -> CoaddProvenance: 

285 """Extract provenance from a legacy 

286 `lsst.cell_coadds.MultipleCellCoadd` object. 

287 

288 Parameters 

289 ---------- 

290 legacy_cell_coadd 

291 Legacy cell coadd to extract provenance from. 

292 """ 

293 inputs = CoaddProvenance.make_empty_input_table(len(legacy_cell_coadd.common.visit_polygons)) 

294 for n, (legacy_identifiers, legacy_polygon) in enumerate( 

295 legacy_cell_coadd.common.visit_polygons.items() 

296 ): 

297 inputs["instrument"][n] = legacy_identifiers.instrument 

298 inputs["visit"][n] = legacy_identifiers.visit 

299 inputs["detector"][n] = legacy_identifiers.detector 

300 inputs["physical_filter"][n] = legacy_identifiers.physical_filter 

301 inputs["day_obs"][n] = legacy_identifiers.day_obs 

302 inputs["polygon"][n] = Polygon.from_legacy(legacy_polygon) 

303 n_contributions = 0 

304 for legacy_cell in legacy_cell_coadd.cells.values(): 

305 n_contributions += len(legacy_cell.inputs) 

306 contributions = CoaddProvenance.make_empty_contribution_table(n_contributions) 

307 n = 0 

308 for legacy_cell in legacy_cell_coadd.cells.values(): 

309 for legacy_identifiers, legacy_inputs in legacy_cell.inputs.items(): 

310 contributions["cell_i"][n] = legacy_cell.identifiers.cell.y 

311 contributions["cell_j"][n] = legacy_cell.identifiers.cell.x 

312 contributions["instrument"][n] = legacy_identifiers.instrument 

313 contributions["visit"][n] = legacy_identifiers.visit 

314 contributions["detector"][n] = legacy_identifiers.detector 

315 contributions["overlaps_center"][n] = legacy_inputs.overlaps_center 

316 contributions["overlap_fraction"][n] = legacy_inputs.overlap_fraction 

317 contributions["unmasked_fraction"][n] = legacy_inputs.unmasked_overlap_fraction 

318 contributions["weight"][n] = legacy_inputs.weight 

319 contributions["psf_shape_xx"][n] = legacy_inputs.psf_shape.getIxx() 

320 contributions["psf_shape_yy"][n] = legacy_inputs.psf_shape.getIyy() 

321 contributions["psf_shape_xy"][n] = legacy_inputs.psf_shape.getIxy() 

322 contributions["psf_shape_flag"][n] = legacy_inputs.psf_shape_flag 

323 n += 1 

324 return CoaddProvenance(inputs=inputs, contributions=contributions) 

325 

326 def to_legacy_polygon_map(self) -> dict[LegacyObservationIdentifiers, LegacyPolygon]: 

327 """Construct a legacy mapping from 

328 `lsst.cell_coadds.ObservationIdentifiers` to `lsst.afw.geom.Polygon` 

329 from the `inputs` table. 

330 """ 

331 from lsst.cell_coadds import ObservationIdentifiers as LegacyObservationIdentifiers 

332 

333 return { 

334 LegacyObservationIdentifiers( 

335 instrument=str(row["instrument"]), 

336 physical_filter=str(row["physical_filter"]), 

337 visit=int(row["visit"]), 

338 day_obs=int(row["day_obs"]), 

339 detector=int(row["detector"]), 

340 ): row["polygon"].to_legacy() 

341 for row in self.inputs 

342 } 

343 

344 def to_legacy_cell_coadd_inputs( 

345 self, observations: Iterable[LegacyObservationIdentifiers] | None 

346 ) -> dict[LegacyIndex2D, dict[LegacyObservationIdentifiers, LegacyCellCoaddInputs]]: 

347 """Construct a mapping from legacy cell index to the list of legacy 

348 input structs for that cell. 

349 

350 Parameters 

351 ---------- 

352 observations 

353 Observations to include, or `None` to include all observations 

354 in the `inputs` table. 

355 """ 

356 from lsst.afw.geom.ellipses import Quadrupole 

357 from lsst.cell_coadds import CoaddInputs as LegacyCoaddInputs 

358 from lsst.skymap import Index2D as LegacyIndex2D 

359 

360 if observations is None: 

361 observations = self.to_legacy_polygon_map().keys() 

362 observations_by_key: dict[tuple[str, int, int], LegacyObservationIdentifiers] = { 

363 (obs.instrument, obs.visit, obs.detector): obs for obs in observations 

364 } 

365 result: dict[LegacyIndex2D, dict[LegacyObservationIdentifiers, LegacyCoaddInputs]] = {} 

366 for row in self.contributions: 

367 obs_key = (str(row["instrument"]), int(row["visit"]), int(row["detector"])) 

368 obs = observations_by_key[obs_key] 

369 cell_inputs = result.setdefault(LegacyIndex2D(x=int(row["cell_j"]), y=int(row["cell_i"])), {}) 

370 cell_inputs[obs] = LegacyCoaddInputs( 

371 overlaps_center=bool(row["overlaps_center"]), 

372 overlap_fraction=float(row["overlap_fraction"]), 

373 unmasked_overlap_fraction=float(row["unmasked_fraction"]), 

374 weight=float(row["weight"]), 

375 psf_shape=Quadrupole(row["psf_shape_xx"], row["psf_shape_yy"], row["psf_shape_xy"]), 

376 psf_shape_flag=bool(row["psf_shape_flag"]), 

377 ) 

378 return result 

379 

380 

381class CoaddProvenanceSerializationModel(ArchiveTree): 

382 """A Pydantic model used to represent a serialized `CoaddProvenance`. 

383 

384 Notes 

385 ----- 

386 We can't rewrite the Astropy tables directly into the archive (e.g. as 

387 FITS binary tables for a FITS archive), because: 

388 

389 - `str` columns are a huge pain in both Numpy and FITS; 

390 - the polygon columns need to be rewritten as array-valued columns. 

391 

392 To deal with the string columns (``instrument`` and ``physical_filter``) 

393 we do dictionary compression: we map each distinct value of those columns 

394 to an integer, and then we save that mapping to the model while saving 

395 an integer version of that column in the table. But if there is actually 

396 only one value in that column (the most common case by far) we just drop 

397 the column and store that value directly in the model. 

398 """ 

399 

400 SCHEMA_NAME: ClassVar[str] = "coadd_provenance" 

401 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

402 MIN_READ_VERSION: ClassVar[int] = 1 

403 PUBLIC_TYPE: ClassVar[type] = CoaddProvenance 

404 

405 instrument: str | dict[str, int] = pydantic.Field( 

406 description=( 

407 "Instrument name for all inputs to this coadd, or a mapping from " 

408 "instrument name to the integer used in its place in the tables." 

409 ) 

410 ) 

411 physical_filter: str | dict[str, int] = pydantic.Field( 

412 description="Physical filter name for all inputs to this coadd." 

413 ) 

414 inputs: TableModel = pydantic.Field(description="Table of all inputs to the coadd.") 

415 contributions: TableModel = pydantic.Field(description="Table of per-cell contributions to the coadd.") 

416 

417 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> CoaddProvenance: 

418 """Deserialize a provenance from an input archive. 

419 

420 Parameters 

421 ---------- 

422 archive 

423 Archive to read from. 

424 **kwargs 

425 Unsupported keyword arguments are accepted only to provide 

426 better error messages (raising 

427 `.serialization.InvalidParameterError`). 

428 

429 Notes 

430 ----- 

431 While `CoaddProvenance.subset` can be used to filter provenance 

432 information down to just certain cells, there is no advantage to be 

433 had from doing this during deserialization (the table data is not 

434 ordered by cell, and hence there's read-slicing we can do). 

435 """ 

436 if kwargs: 436 ↛ 437line 436 didn't jump to line 437 because the condition on line 436 was never true

437 raise InvalidParameterError(f"Unrecognized parameters for CoaddProvenance: {set(kwargs.keys())}.") 

438 inputs = archive.get_table(self.inputs) 

439 contributions = archive.get_table(self.contributions) 

440 CoaddProvenanceSerializationModel._fix_str_for_deserialization( 

441 "instrument", self.instrument, inputs, contributions 

442 ) 

443 CoaddProvenanceSerializationModel._fix_str_for_deserialization( 

444 "physical_filter", self.physical_filter, inputs 

445 ) 

446 CoaddProvenanceSerializationModel._fix_polygon_for_deserialization(inputs) 

447 for name, _, description in CoaddProvenance._INPUT_TABLE_COLUMNS: 

448 inputs.columns[name].description = description 

449 for name, _, description, unit in CoaddProvenance._CONTRIBUTION_TABLE_COLUMNS: 

450 contributions.columns[name].description = description 

451 contributions.columns[name].unit = unit 

452 return CoaddProvenance(inputs=inputs, contributions=contributions) 

453 

454 @staticmethod 

455 def _fix_str_for_serialization(column: str, *tables: astropy.table.Table) -> str | dict[str, int]: 

456 """Rewrite a string column as an integer column or drop it. 

457 

458 Parameters 

459 ---------- 

460 column 

461 Name of the column to rewrite. 

462 *tables 

463 One or more astropy tables to rewrite. The first table is assumed 

464 to have all values for this column that might appear in any other 

465 tables. 

466 

467 Returns 

468 ------- 

469 `str` | `dict` [`str`, `int`] 

470 If there is only one unique value for this column in the first 

471 table, that value (and the column will have been dropped from 

472 all givne tables). If the tables are empty, the column is 

473 dropped and an empty `dict` is returned. In all other cases the 

474 given column is replaced with an integer column in all given 

475 tables and the mapping from strings to integers is returned. 

476 """ 

477 result: str | dict[str, int] = {name: n for n, name in enumerate(sorted(set(tables[0][column])))} 

478 match len(result): 

479 case 0: 479 ↛ 480line 479 didn't jump to line 480 because the pattern on line 479 never matched

480 pass 

481 case 1: 481 ↛ 483line 481 didn't jump to line 483 because the pattern on line 481 always matched

482 (result,) = result.keys() # type: ignore[union-attr] 

483 case _: 

484 for table in tables: 

485 table.columns[column] = astropy.table.Column( 

486 data=[result[k] for k in table.columns[column]], 

487 name=column, 

488 dtype=np.uint8, 

489 description=f"Integer mapped to {column} name.", 

490 ) 

491 return result 

492 # If we didn't remap to an integer (case 0 and 1 above), delete the 

493 # column. 

494 for table in tables: 

495 del table.columns[column] 

496 return result 

497 

498 @staticmethod 

499 def _fix_str_for_deserialization( 

500 column: str, value: str | dict[str, int], *tables: astropy.table.Table 

501 ) -> None: 

502 """Rewrite an integer column back to a string one. 

503 

504 Parameters 

505 ---------- 

506 column 

507 Name of the column to rewrite. 

508 value 

509 Value or mapping of values returned by 

510 `_fix_str_for_serialization`. 

511 tables 

512 Tables to rewrite this column in. 

513 """ 

514 match value: 

515 case str(): 515 ↛ 518line 515 didn't jump to line 518 because the pattern on line 515 always matched

516 for table in tables: 

517 table.columns[column] = astropy.table.Column([value] * len(table), dtype=object) 

518 case dict(): 

519 mapping = {v: k for k, v in value.items()} 

520 for table in tables: 

521 table.columns[column] = astropy.table.Column( 

522 [mapping[k] for k in table[column]], dtype=object 

523 ) 

524 

525 @staticmethod 

526 def _fix_polygon_for_serialization(inputs: astropy.table.Table) -> None: 

527 """Rewrite a polygon `object` column as a pair of array-valued columns 

528 and an array-size column. 

529 

530 Parameters 

531 ---------- 

532 inputs 

533 A copy of the in-memory coadd inputs table to modify in-place into 

534 its serialization form. 

535 """ 

536 max_n_vertices = max(p.n_vertices for p in inputs["polygon"]) 

537 inputs["n_vertices"] = astropy.table.Column( 

538 [p.n_vertices for p in inputs["polygon"]], 

539 name="n_vertices", 

540 dtype=np.uint8, 

541 description="Number of polygon vertices.", 

542 ) 

543 inputs["x_vertices"] = astropy.table.Column( 

544 name="x_vertices", 

545 dtype=np.float64, 

546 length=len(inputs), 

547 shape=(max_n_vertices,), 

548 description="X coordinates of polygon vertices, in tract coordinates.", 

549 ) 

550 inputs["x_vertices"][:, :] = np.nan 

551 inputs["y_vertices"] = astropy.table.Column( 

552 name="y_vertices", 

553 dtype=np.float64, 

554 length=len(inputs), 

555 shape=(max_n_vertices,), 

556 description="Y coordinates of polygon vertices, in tract coordinates.", 

557 ) 

558 inputs["y_vertices"][:, :] = np.nan 

559 for i, polygon in enumerate(inputs["polygon"]): 

560 inputs["n_vertices"][i] = polygon.n_vertices 

561 inputs["x_vertices"][i][: polygon.n_vertices] = polygon.x_vertices 

562 inputs["y_vertices"][i][: polygon.n_vertices] = polygon.y_vertices 

563 del inputs["polygon"] 

564 

565 @staticmethod 

566 def _fix_polygon_for_deserialization(inputs: astropy.table.Table) -> None: 

567 """Rewrite a a pair of array-valued columns and an array-size column 

568 into a polygon `object` column. 

569 

570 Parameters 

571 ---------- 

572 inputs 

573 The serialized version of the coadd inputs table, to be modified 

574 in-place into its in-memory form. 

575 """ 

576 polygons = [ 

577 Polygon(x_vertices=x_vertices[:n_vertices], y_vertices=y_vertices[:n_vertices]) 

578 for n_vertices, x_vertices, y_vertices in zip( 

579 inputs["n_vertices"], inputs["x_vertices"], inputs["y_vertices"] 

580 ) 

581 ] 

582 del inputs["n_vertices"] 

583 del inputs["x_vertices"] 

584 del inputs["y_vertices"] 

585 inputs["polygon"] = astropy.table.Column(polygons, name="polygon", dtype=np.object_)