Coverage for tests / test_visit_geometry.py: 18%
73 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:20 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 08:20 +0000
1# This file is part of obs_base.
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# 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/>.
22import tempfile
23import unittest
25import numpy as np
27from lsst.daf.butler import Butler, DatasetType, DimensionRecord
28from lsst.obs.base.visit_geometry import VisitGeometry
29from lsst.sphgeom import Angle, ConvexPolygon, LonLat, UnitVector3d
32class VisitGeometryTestCase(unittest.TestCase):
33 """Tests for VisitGeometry."""
35 def setUp(self):
36 root = self.enterContext(tempfile.TemporaryDirectory(ignore_cleanup_errors=True))
37 Butler.makeRepo(root)
38 self.butler: Butler = self.enterContext(Butler.from_config(root, run="geometry"))
39 self.rng = np.random.default_rng(500)
40 self.rot_about = UnitVector3d(*self.rng.uniform(0.0, 1.0, size=3).tolist())
41 self.rot_amount = Angle.fromDegrees(self.rng.uniform(0.0, 5.0))
43 def test_round_trip(self):
44 """Test round-tripping through a JSON string."""
45 self.butler.import_(filename="resource://lsst.daf.butler/tests/registry_data/base.yaml")
46 self.butler.import_(filename="resource://lsst.daf.butler/tests/registry_data/spatial.yaml")
47 (visit_record,) = self.butler.query_dimension_records("visit", visit=1, instrument="Cam1")
48 visit_detector_region_records = self.butler.query_dimension_records(
49 "visit_detector_region", visit=1, instrument="Cam1"
50 )
51 vg1 = VisitGeometry(
52 boresight_ra=45.0,
53 boresight_dec=60.0,
54 orientation=30.0,
55 visit_region=visit_record.region,
56 detector_regions={r.detector: r.region for r in visit_detector_region_records},
57 )
58 json_data = vg1.model_dump_json()
59 vg2 = VisitGeometry.model_validate_json(json_data)
60 self.assertEqual(vg1.boresight_ra, vg2.boresight_ra)
61 self.assertEqual(vg1.boresight_dec, vg2.boresight_dec)
62 self.assertEqual(vg1.orientation, vg2.orientation)
63 self.assertEqual(vg1.visit_region, vg2.visit_region)
64 self.assertEqual(vg1.detector_regions, vg2.detector_regions)
66 def test_update_dimension_records(self):
67 """Test the update_dimension_records method."""
68 self.butler.import_(filename="resource://lsst.daf.butler/tests/registry_data/base.yaml")
69 self.butler.import_(filename="resource://lsst.daf.butler/tests/registry_data/spatial.yaml")
70 old_visit_records = {r.id: r for r in self.butler.query_dimension_records("visit")}
71 old_exposure_records = {
72 r.id: self._invent_and_insert_exposure_record(r) for r in old_visit_records.values()
73 }
74 self.butler.registry.registerDatasetType(
75 DatasetType("visit_geometry", self.butler.dimensions.conform(["visit"]), "VisitGeometry")
76 )
77 visit_geometries: dict[int, VisitGeometry] = {}
78 self.assertEqual(len(old_visit_records), 2)
79 for visit_id, visit_record in old_visit_records.items():
80 exposure_record = old_exposure_records[visit_id]
81 old_boresight = LonLat.fromDegrees(exposure_record.tracking_ra, exposure_record.tracking_dec)
82 new_boresight = LonLat(self._modify_point(UnitVector3d(old_boresight)))
83 visit_geometry = VisitGeometry(
84 boresight_ra=new_boresight.getLon().asDegrees(),
85 boresight_dec=new_boresight.getLat().asDegrees(),
86 # We *mostly* try to modify the geometry self-consistently, but
87 # the orientation angle is just arbitrary. Nothing in the test
88 # right now requires any kind of consistency between the
89 # regions and the exposure angle fields.
90 orientation=self.rng.uniform(0.0, 5.0),
91 visit_region=self._modify_polygon(visit_record.region),
92 detector_regions={
93 r.detector: self._modify_polygon(r.region)
94 for r in self.butler.query_dimension_records(
95 "visit_detector_region", data_id=visit_record.dataId
96 )
97 },
98 )
99 visit_geometries[visit_id] = visit_geometry
100 self.butler.put(visit_geometry, "visit_geometry", visit_record.dataId)
101 n_updated = VisitGeometry.update_dimension_records(self.butler, instrument="Cam1")
102 self.assertEqual(n_updated, 2)
103 for new_visit_record in self.butler.query_dimension_records("visit"):
104 visit_id = new_visit_record.id
105 old_visit_record = old_visit_records[visit_id]
106 visit_geometry = visit_geometries[visit_id]
107 self.assertNotEqual(new_visit_record.region, old_visit_record.region)
108 self.assertEqual(new_visit_record.region, visit_geometry.visit_region)
109 for new_vdr_record in self.butler.query_dimension_records(
110 "visit_detector_region", data_id=new_visit_record.dataId
111 ):
112 self.assertEqual(
113 new_vdr_record.region, visit_geometry.detector_regions[new_vdr_record.detector]
114 )
115 for new_exposure_record in self.butler.query_dimension_records("exposure"):
116 visit_id = new_exposure_record.id
117 visit_geometry = visit_geometries[visit_id]
118 self.assertEqual(new_exposure_record.tracking_ra, visit_geometry.boresight_ra)
119 self.assertEqual(new_exposure_record.tracking_dec, visit_geometry.boresight_dec)
120 self.assertEqual(new_exposure_record.sky_angle, visit_geometry.orientation)
122 def _invent_and_insert_exposure_record(self, visit_record: DimensionRecord) -> DimensionRecord:
123 group_record = self.butler.dimensions["group"].RecordClass(
124 instrument=visit_record.instrument, name=str(visit_record.id)
125 )
126 self.butler.registry.insertDimensionData("group", group_record, skip_existing=True)
127 center = LonLat(visit_record.region.getCentroid())
128 exposure_record = self.butler.dimensions["exposure"].RecordClass(
129 instrument=visit_record.instrument,
130 id=visit_record.id,
131 day_obs=visit_record.day_obs,
132 group=group_record.name,
133 physical_filter=visit_record.physical_filter,
134 tracking_ra=center.getLon().asDegrees(),
135 tracking_dec=center.getLat().asDegrees(),
136 sky_angle=float(self.rng.uniform(0.0, 5.0)),
137 )
138 self.butler.registry.insertDimensionData("exposure", exposure_record)
139 visit_definition_record = self.butler.dimensions["visit_definition"].RecordClass(
140 instrument=visit_record.instrument,
141 visit=visit_record.id,
142 exposure=exposure_record.id,
143 )
144 self.butler.registry.insertDimensionData("visit_definition", visit_definition_record)
145 return exposure_record
147 def _modify_polygon(self, polygon: ConvexPolygon) -> ConvexPolygon:
148 return ConvexPolygon([self._modify_point(v) for v in polygon.getVertices()])
150 def _modify_point(self, u: UnitVector3d) -> UnitVector3d:
151 return u.rotatedAround(self.rot_about, self.rot_amount)
154if __name__ == "__main__": 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 unittest.main()