Coverage for python / lsst / summit / utils / m1m3 / plots / plot_ics.py: 23%

105 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-22 09:17 +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/>. 

21from __future__ import annotations 

22 

23import logging 

24from typing import TYPE_CHECKING, Any, Optional, TypedDict 

25 

26import matplotlib.pyplot as plt 

27import pandas as pd 

28from astropy.time import Time 

29from matplotlib.patches import Patch 

30 

31if TYPE_CHECKING: 

32 from matplotlib.figure import Figure 

33 

34 from ..inertia_compensation_system import M1M3ICSAnalysis 

35 

36__all__ = [ 

37 "plot_hp_data", 

38 "mark_slew_begin_end", 

39 "mark_padded_slew_begin_end", 

40 "customize_fig", 

41 "customize_hp_plot", 

42 "add_hp_limits", 

43 "plot_velocity_data", 

44 "plot_torque_data", 

45 "plot_stable_region", 

46 "plot_hp_measured_data", 

47 "HP_BREAKAWAY_LIMIT", 

48 "HP_FATIGUE_LIMIT", 

49 "HP_OPERATIONAL_LIMIT", 

50 "FIGURE_WIDTH", 

51 "FIGURE_HEIGHT", 

52] 

53 

54# Approximate value for breakaway 

55HP_BREAKAWAY_LIMIT: float = 3000 # [N] 

56 

57# limit that can still damage the mirror with fatigue 

58HP_FATIGUE_LIMIT: float = 900 # [N] 

59 

60# desired operational limit 

61HP_OPERATIONAL_LIMIT: float = 450 # [N] 

62 

63FIGURE_WIDTH = 10 

64FIGURE_HEIGHT = 7 

65 

66 

67class HPLimitsDict(TypedDict, total=False): 

68 pos_limit: float 

69 neg_limit: float 

70 ls: str 

71 

72 

73def plot_hp_data(ax: plt.Axes, data: pd.Series | list, label: str) -> plt.Line2D: 

74 """ 

75 Plot hardpoint data on the given axes. 

76 

77 Parameters 

78 ---------- 

79 ax : `plt.Axes` 

80 The axes on which the data is plotted. 

81 topic : `str` 

82 The topic of the data. 

83 data : `Series` or `list` 

84 The data points to be plotted. 

85 label : `str` 

86 The label for the plotted data. 

87 

88 Returns 

89 ------- 

90 lines : `plt.Line2D` 

91 The plotted data as a Line2D object. 

92 """ 

93 line = ax.plot(data, "-", label=label, lw=0.5) 

94 # Make this function consistent with others by returning single Line2D 

95 return line[0] 

96 

97 

98def mark_slew_begin_end(ax: plt.Axes, slew_begin: Time, slew_end: Time) -> plt.Line2D: 

99 """ 

100 Mark the beginning and the end of a slew with vertical lines on the given 

101 axes. 

102 

103 Parameters 

104 ---------- 

105 ax : `matplotlib.axes._axes.Axes` 

106 The axes where the vertical lines are drawn. 

107 slew_begin : `astropy.time.Time` 

108 The slew beginning time. 

109 slew_end : `astropy.time.Time` 

110 The slew ending time. 

111 

112 Returns 

113 ------- 

114 line : `matplotlib.lines.Line2D` 

115 The Line2D object representing the line drawn at the slew end. 

116 """ 

117 _ = ax.axvline(slew_begin.datetime, lw=0.5, ls="--", c="k", zorder=-1) 

118 line = ax.axvline(slew_end.datetime, lw=0.5, ls="--", c="k", zorder=-1, label="Slew Start/Stop") 

119 return line 

120 

121 

122def mark_padded_slew_begin_end(ax: plt.Axes, begin: Time, end: Time) -> plt.Line2D: 

123 """ 

124 Mark the padded beginning and the end of a slew with vertical lines. 

125 

126 Parameters 

127 ---------- 

128 ax : `matplotlib.axes._axes.Axes` 

129 The axes where the vertical lines are drawn. 

130 begin : `astropy.time.Time` 

131 The padded slew beginning time. 

132 end : `astropy.time.Time` 

133 The padded slew ending time. 

134 

135 Returns 

136 ------- 

137 line : `matplotlib.lines.Line2D` 

138 The Line2D object representing the line drawn at the padded slew end. 

139 """ 

140 _ = ax.axvline(begin.datetime, alpha=0.5, lw=0.5, ls="-", c="k", zorder=-1) 

141 line = ax.axvline( 

142 end.datetime, 

143 alpha=0.5, 

144 lw=0.5, 

145 ls="-", 

146 c="k", 

147 zorder=-1, 

148 label="Padded Slew Start/Stop", 

149 ) 

150 return line 

151 

152 

153def customize_fig(fig: plt.Figure, dataset: M1M3ICSAnalysis) -> None: 

154 """ 

155 Add a title to a figure and adjust its subplots spacing 

156 

157 Paramters 

158 --------- 

159 fig : `matplotlib.pyplot.Figure` 

160 Figure to be custoized. 

161 dataset : `M1M3ICSAnalysis` 

162 The dataset object containing the data to be plotted and metadata. 

163 """ 

164 t_fmt = "%Y%m%d %H:%M:%S" 

165 fig.suptitle( 

166 f"HP Measured Data\n " 

167 f"DayObs {dataset.event.dayObs} " 

168 f"SeqNum {dataset.event.seqNum} " 

169 f"v{dataset.event.version}\n " 

170 f"{dataset.df.index[0].strftime(t_fmt)} - " 

171 f"{dataset.df.index[-1].strftime(t_fmt)}" 

172 ) 

173 

174 fig.subplots_adjust(hspace=0) 

175 

176 

177def customize_hp_plot(ax: plt.Axes, lines: list[plt.Line2D]) -> None: 

178 """ 

179 Customize the appearance of the hardpoint plot. 

180 

181 Parameters 

182 ---------- 

183 ax : `matplotlib.axes._axes.Axes` 

184 The axes of the plot to be customized. 

185 lines : `list` 

186 The list of Line2D objects representing the plotted data lines. 

187 """ 

188 limit_lines = add_hp_limits(ax) 

189 lines.extend(limit_lines) 

190 

191 ax.set_xlabel("Time [UTC]") 

192 ax.set_ylabel("HP Measured\n Forces [N]") 

193 ax.set_ylim(-3100, 3100) 

194 ax.grid(linestyle=":", alpha=0.2) 

195 

196 

197def add_hp_limits(ax: plt.Axes) -> list[plt.Line2D]: 

198 """ 

199 Add horizontal lines to represent the breakaway limits, the fatigue limits, 

200 and the operational limits. 

201 

202 This was first discussed on Slack. From Doug Neil we got: 

203 

204 > A fracture statistics estimate of the fatigue limit of a borosilicate 

205 > glass. The fatigue limit of borosilicate glass is 0.21 MPa (~30 psi). 

206 > This implies that repeated loads of 30% of our breakaway limit would 

207 > eventually produce failure. To ensure that the system is safe for the 

208 > life of the project we should provide a factor of safety of at least two. 

209 > I recommend a 30% repeated load limit, and a project goal to keep the 

210 > stress below 15% of the breakaway during normal operations. 

211 

212 Parameters 

213 ---------- 

214 ax : `plt.Axes` 

215 The axes on which the velocity data is plotted. 

216 """ 

217 hp_limits: dict[str, HPLimitsDict] = { 

218 "HP Breakaway Limit": { 

219 "pos_limit": HP_BREAKAWAY_LIMIT, 

220 "neg_limit": -HP_BREAKAWAY_LIMIT, 

221 "ls": "-", 

222 }, 

223 "Repeated Load Limit (30% breakaway)": { 

224 "pos_limit": HP_FATIGUE_LIMIT, 

225 "neg_limit": -HP_FATIGUE_LIMIT, 

226 "ls": "--", 

227 }, 

228 "Normal Ops Limit (15% breakaway)": { 

229 "pos_limit": HP_OPERATIONAL_LIMIT, 

230 "neg_limit": -HP_OPERATIONAL_LIMIT, 

231 "ls": ":", 

232 }, 

233 } 

234 

235 kwargs: dict[str, Any] = dict(alpha=0.5, lw=1.0, c="r", zorder=-1) 

236 line_list = [] 

237 

238 for key, sub_dict in hp_limits.items(): 

239 ax.axhline(sub_dict["pos_limit"], ls=sub_dict["ls"], **kwargs) 

240 line = ax.axhline(sub_dict["neg_limit"], ls=sub_dict["ls"], label=key, **kwargs) 

241 line_list.append(line) 

242 

243 return line_list 

244 

245 

246def plot_velocity_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None: 

247 """ 

248 Plot the azimuth and elevation velocities on the given axes. 

249 

250 Parameters 

251 ---------- 

252 ax : `matplotlib.axes._axes.Axes` 

253 The axes on which the velocity data is plotted. 

254 dataset : `M1M3ICSAnalysis` 

255 The dataset object containing the data to be plotted and metadata. 

256 """ 

257 ax.plot(dataset.df["az_actual_velocity"], color="royalblue", label="Az Velocity") 

258 ax.plot(dataset.df["el_actual_velocity"], color="teal", label="El Velocity") 

259 ax.grid(linestyle=":", alpha=0.2) 

260 ax.set_ylabel("Actual Velocity\n [deg/s]") 

261 ax.legend(ncol=2, fontsize="x-small") 

262 

263 

264def plot_torque_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None: 

265 """ 

266 Plot the azimuth and elevation torques on the given axes. 

267 

268 Parameters 

269 ---------- 

270 ax : `matplotlib.axes._axes.Axes` 

271 The axes on which the torque data is plotted. 

272 dataset : `M1M3ICSAnalysis` 

273 The dataset object containing the data to be plotted and metadata. 

274 """ 

275 ax.plot(dataset.df["az_actual_torque"], color="firebrick", label="Az Torque") 

276 ax.plot(dataset.df["el_actual_torque"], color="salmon", label="El Torque") 

277 ax.grid(linestyle=":", alpha=0.2) 

278 ax.set_ylabel("Actual Torque\n [kN.m]") 

279 ax.legend(ncol=2, fontsize="x-small") 

280 

281 

282def plot_stable_region( 

283 fig: plt.Figure, begin: Time, end: Time, label: str = "", color: str = "b" 

284) -> Optional[Patch]: 

285 """Highlight a stable region on the plot with a colored span. 

286 

287 Parameters 

288 ---------- 

289 fig : `plt.Figure` 

290 The figure containing the axes on which the stable region is 

291 highlighted. 

292 begin : `astropy.time.Time` 

293 The beginning time of the stable region. 

294 end : `astropy.time.Time` 

295 The ending time of the stable region. 

296 label : `str`, optional 

297 The label for the highlighted region. 

298 color : `str`, optional 

299 The color of the highlighted region. 

300 

301 Returns 

302 ------- 

303 patch : `matplotlib.patches.Patch` 

304 The patch object representing the highlighted region. 

305 """ 

306 span = None # Fixes mypy error about uninitialized variable 

307 for ax in fig.axes[1:]: 

308 span = ax.axvspan(begin.datetime, end.datetime, fc=color, alpha=0.1, zorder=-2, label=label) 

309 return span 

310 

311 

312def plot_hp_measured_data( 

313 dataset: M1M3ICSAnalysis, 

314 fig: plt.Figure, 

315 commands: dict[Time, str] | None = None, 

316 log: logging.Logger | None = None, 

317) -> Figure: 

318 """ 

319 Create and plot hardpoint measured data, velocity, and torque on subplots. 

320 This plot was designed for a figure with `figsize=(10, 7)` and `dpi=120`. 

321 

322 Parameters 

323 ---------- 

324 dataset : `M1M3ICSAnalysis` 

325 The dataset object containing the data to be plotted and metadata. 

326 fig : `plt.Figure` 

327 The figure to be plotted on. 

328 commands : `dict`, optional 

329 A dictionary times at which commands were issued, and with the values 

330 as the command strings themselves. 

331 log : `logging.Logger`, optional 

332 The logger object to log progress. 

333 """ 

334 log = log.getChild(__name__) if log is not None else logging.getLogger(__name__) 

335 

336 # Start clean 

337 fig.clear() 

338 

339 # Add subplots 

340 gs = fig.add_gridspec(4, 1, height_ratios=[1, 2, 1, 1]) 

341 

342 ax_label = fig.add_subplot(gs[0]) 

343 ax_hp = fig.add_subplot(gs[1]) 

344 ax_tor = fig.add_subplot(gs[2], sharex=ax_hp) 

345 ax_vel = fig.add_subplot(gs[3], sharex=ax_hp) 

346 

347 # Remove frame from axis dedicated to label 

348 ax_label.axis("off") 

349 

350 # Plotting 

351 line_list: list[plt.Line2D] = [] 

352 for hp in range(dataset.number_of_hardpoints): 

353 topic = dataset.measured_forces_topics[hp] 

354 line = plot_hp_data(ax_hp, dataset.df[topic], f"HP{hp + 1}") 

355 line_list.append(line) 

356 

357 slew_begin = Time(dataset.event.begin, scale="utc") 

358 slew_end = Time(dataset.event.end, scale="utc") 

359 

360 mark_slew_begin_end(ax_hp, slew_begin, slew_end) 

361 mark_slew_begin_end(ax_vel, slew_begin, slew_end) 

362 line = mark_slew_begin_end(ax_tor, slew_begin, slew_end) 

363 line_list.append(line) 

364 

365 mark_padded_slew_begin_end(ax_hp, slew_begin - dataset.outer_pad, slew_end + dataset.outer_pad) 

366 mark_padded_slew_begin_end(ax_vel, slew_begin - dataset.outer_pad, slew_end + dataset.outer_pad) 

367 line = mark_padded_slew_begin_end(ax_tor, slew_begin - dataset.outer_pad, slew_end + dataset.outer_pad) 

368 line_list.append(line) 

369 

370 plot_velocity_data(ax_vel, dataset) 

371 plot_torque_data(ax_tor, dataset) 

372 

373 lineColors = [p["color"] for p in plt.rcParams["axes.prop_cycle"]] # cycle through the colors 

374 colorCounter = 0 

375 if commands is not None: 

376 for commandTime, command in commands.items(): 

377 command = command.replace("lsst.sal.", "") 

378 

379 for ax in (ax_hp, ax_tor, ax_vel): # so that the line spans all plots 

380 line = ax.axvline( 

381 commandTime, 

382 c=lineColors[colorCounter], 

383 ls="--", 

384 alpha=0.75, 

385 label=f"{command}", 

386 ) 

387 line_list.append(line) # put it in the legend 

388 colorCounter += 1 # increment color so each line is different 

389 

390 customize_hp_plot(ax_hp, line_list) 

391 

392 handles, labels = ax_hp.get_legend_handles_labels() 

393 ax_label.legend(handles, labels, loc="center", frameon=False, ncol=4, fontsize="x-small") 

394 

395 customize_fig(fig, dataset) 

396 

397 return fig