Coverage for python/lsst/summit/utils/simonyi/mountData.py: 0%
73 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-06 02:44 -0700
« prev ^ index » next coverage.py v7.15.4, created at 2026-09-06 02:44 -0700
1# This file is part of summit_utils.
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 <https://www.gnu.org/licenses/>.
22from __future__ import annotations
24__all__ = [
25 "MountData",
26 "getAzElRotHexDataForPeriod",
27 "getAzElRotHexDataForExposure",
28]
30from collections.abc import Sequence
31from dataclasses import dataclass
32from typing import TYPE_CHECKING
34import numpy as np
35from scipy.optimize import minimize
37from ..efdUtils import getEfdData
39if TYPE_CHECKING:
40 from astropy.time import Time
41 from lsst_efd_client import EfdClient
42 from pandas import DataFrame
44 from lsst.daf.butler import DimensionRecord
47@dataclass
48class MountData:
49 begin: Time
50 end: Time
51 azimuthData: DataFrame
52 elevationData: DataFrame
53 rotationData: DataFrame
54 rotationTorques: DataFrame
55 camhexData: DataFrame
56 m2hexData: DataFrame
57 includedPrePadding: float
58 includedPostPadding: float
59 expRecord: DimensionRecord | None
61 @property
62 def empty(self) -> bool:
63 """Return True if the data is empty."""
64 return (
65 self.azimuthData.empty
66 and self.elevationData.empty
67 and self.rotationData.empty
68 and self.rotationTorques.empty
69 )
72def getAzElRotHexDataForPeriod(
73 client: EfdClient,
74 begin: Time,
75 end: Time,
76 prePadding: float = 0,
77 postPadding: float = 0,
78 maxDeltaT: float = 1.0e-3,
79) -> MountData:
80 azimuthData = getEfdData(
81 client,
82 "lsst.sal.MTMount.azimuth",
83 begin=begin,
84 end=end,
85 prePadding=prePadding,
86 postPadding=postPadding,
87 )
88 elevationData = getEfdData(
89 client,
90 "lsst.sal.MTMount.elevation",
91 begin=begin,
92 end=end,
93 prePadding=prePadding,
94 postPadding=postPadding,
95 )
96 rotationData = getEfdData(
97 client,
98 "lsst.sal.MTRotator.rotation",
99 begin=begin,
100 end=end,
101 prePadding=prePadding,
102 postPadding=postPadding,
103 )
104 rotationTorques = getEfdData(
105 client,
106 "lsst.sal.MTRotator.motors",
107 begin=begin,
108 end=end,
109 prePadding=prePadding,
110 postPadding=postPadding,
111 )
112 hexData = getEfdData(
113 client,
114 "lsst.sal.MTHexapod.application",
115 begin=begin,
116 end=end,
117 prePadding=prePadding,
118 postPadding=postPadding,
119 )
120 camhexData = hexData[hexData["salIndex"] == 1]
121 m2hexData = hexData[hexData["salIndex"] == 2]
123 def calcDeltaT(params: Sequence[float], args: Sequence[np.ndarray]) -> float:
124 # This calculates the deltaT needed
125 # to make the median(error) = 0
126 [values, valTimes, demand, demTimes] = args
127 [deltaT] = params
128 demandInterp = np.interp(valTimes, demTimes + deltaT, demand)
129 error = (values - demandInterp) * 3600
130 value = abs(np.median(error))
131 return float(value)
133 azValues = np.asarray(azimuthData["actualPosition"])
134 azValTimes = np.asarray(azimuthData["actualPositionTimestamp"])
135 azDemand = np.asarray(azimuthData["demandPosition"])
136 azDemTimes = np.asarray(azimuthData["demandPositionTimestamp"])
137 elValues = np.asarray(elevationData["actualPosition"])
138 elValTimes = np.asarray(elevationData["actualPositionTimestamp"])
139 elDemand = np.asarray(elevationData["demandPosition"])
140 elDemTimes = np.asarray(elevationData["demandPositionTimestamp"])
142 # Calculate the deltaT needed to drive the median(error) to zero
143 args = [azValues, azValTimes, azDemand, azDemTimes]
144 x0 = [0.0]
145 result = minimize(calcDeltaT, x0, args=args, method="Powell", bounds=[(-maxDeltaT, maxDeltaT)])
146 deltaTAz = result.x[0]
148 args = [elValues, elValTimes, elDemand, elDemTimes]
149 x0 = [0.0]
150 result = minimize(calcDeltaT, x0, args=args, method="Powell", bounds=[(-maxDeltaT, maxDeltaT)])
151 deltaTEl = result.x[0]
153 azDemandInterp = np.interp(azValTimes, azDemTimes + deltaTAz, azDemand)
154 elDemandInterp = np.interp(elValTimes, elDemTimes + deltaTEl, elDemand)
156 azError = (azValues - azDemandInterp) * 3600
157 elError = (elValues - elDemandInterp) * 3600
159 rotValues = np.asarray(rotationData["actualPosition"])
160 rotDemand = np.asarray(rotationData["demandPosition"])
161 rotError = (rotValues - rotDemand) * 3600
163 azimuthData["azError"] = azError
164 elevationData["elError"] = elError
165 rotationData["rotError"] = rotError
167 mountData = MountData(
168 begin,
169 end,
170 azimuthData,
171 elevationData,
172 rotationData,
173 rotationTorques,
174 camhexData,
175 m2hexData,
176 prePadding,
177 postPadding,
178 None,
179 )
180 return mountData
183def getAzElRotHexDataForExposure(
184 client: EfdClient, expRecord: DimensionRecord, prePadding: float = 0, postPadding: float = 0
185) -> MountData:
187 begin = expRecord.timespan.begin
188 end = expRecord.timespan.end
189 mountData = getAzElRotHexDataForPeriod(client, begin, end, prePadding, postPadding)
190 mountData.expRecord = expRecord
191 return mountData