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