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

107 statements  

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

49 "HP_FATIGUE_LIMIT_TENSION", 

50 "HP_OPERATIONAL_LIMIT_COMPRESSION", 

51 "HP_OPERATIONAL_LIMIT_TENSION", 

52 "FIGURE_WIDTH", 

53 "FIGURE_HEIGHT", 

54] 

55 

56# Approximate value for breakaway 

57HP_BREAKAWAY_LIMIT: float = 3000 # [N] 

58 

59# limit that can still damage the mirror with fatigue 

60HP_FATIGUE_LIMIT_COMPRESSION: float = 1042 # [N] 

61HP_FATIGUE_LIMIT_TENSION: float = -1163.25 # [N] 

62 

63# desired operational limit 

64HP_OPERATIONAL_LIMIT_COMPRESSION: float = 521 # [N] 

65HP_OPERATIONAL_LIMIT_TENSION: float = -581 # [N] 

66 

67FIGURE_WIDTH = 10 

68FIGURE_HEIGHT = 7 

69 

70 

71class HPLimitsDict(TypedDict, total=False): 

72 pos_limit: float 

73 neg_limit: float 

74 ls: str 

75 

76 

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

78 """ 

79 Plot hardpoint data on the given axes. 

80 

81 Parameters 

82 ---------- 

83 ax : `plt.Axes` 

84 The axes on which the data is plotted. 

85 topic : `str` 

86 The topic of the data. 

87 data : `Series` or `list` 

88 The data points to be plotted. 

89 label : `str` 

90 The label for the plotted data. 

91 

92 Returns 

93 ------- 

94 lines : `plt.Line2D` 

95 The plotted data as a Line2D object. 

96 """ 

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

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

99 return line[0] 

100 

101 

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

103 """ 

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

105 axes. 

106 

107 Parameters 

108 ---------- 

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

110 The axes where the vertical lines are drawn. 

111 slew_begin : `astropy.time.Time` 

112 The slew beginning time. 

113 slew_end : `astropy.time.Time` 

114 The slew ending time. 

115 

116 Returns 

117 ------- 

118 line : `matplotlib.lines.Line2D` 

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

120 """ 

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

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

123 return line 

124 

125 

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

127 """ 

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

129 

130 Parameters 

131 ---------- 

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

133 The axes where the vertical lines are drawn. 

134 begin : `astropy.time.Time` 

135 The padded slew beginning time. 

136 end : `astropy.time.Time` 

137 The padded slew ending time. 

138 

139 Returns 

140 ------- 

141 line : `matplotlib.lines.Line2D` 

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

143 """ 

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

145 line = ax.axvline( 

146 end.datetime, 

147 alpha=0.5, 

148 lw=0.5, 

149 ls="-", 

150 c="k", 

151 zorder=-1, 

152 label="Padded Slew Start/Stop", 

153 ) 

154 return line 

155 

156 

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

158 """ 

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

160 

161 Paramters 

162 --------- 

163 fig : `matplotlib.pyplot.Figure` 

164 Figure to be custoized. 

165 dataset : `M1M3ICSAnalysis` 

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

167 """ 

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

169 fig.suptitle( 

170 f"HP Measured Data\n " 

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

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

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

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

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

176 ) 

177 

178 fig.subplots_adjust(hspace=0) 

179 

180 

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

182 """ 

183 Customize the appearance of the hardpoint plot. 

184 

185 Parameters 

186 ---------- 

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

188 The axes of the plot to be customized. 

189 lines : `list` 

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

191 """ 

192 limit_lines = add_hp_limits(ax) 

193 lines.extend(limit_lines) 

194 

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

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

197 ax.set_ylim(-3100, 3100) 

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

199 

200 

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

202 """ 

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

204 and the operational limits. 

205 

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

207 

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

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

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

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

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

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

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

215 

216 Parameters 

217 ---------- 

218 ax : `plt.Axes` 

219 The axes on which the velocity data is plotted. 

220 """ 

221 hp_limits: dict[str, HPLimitsDict] = { 

222 "HP Breakaway Limit": { 

223 "pos_limit": HP_BREAKAWAY_LIMIT, 

224 "neg_limit": -HP_BREAKAWAY_LIMIT, 

225 "ls": "-", 

226 }, 

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

228 "pos_limit": HP_FATIGUE_LIMIT_COMPRESSION, 

229 "neg_limit": HP_FATIGUE_LIMIT_TENSION, 

230 "ls": "--", 

231 }, 

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

233 "pos_limit": HP_OPERATIONAL_LIMIT_COMPRESSION, 

234 "neg_limit": HP_OPERATIONAL_LIMIT_TENSION, 

235 "ls": ":", 

236 }, 

237 } 

238 

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

240 line_list = [] 

241 

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

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

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

245 line_list.append(line) 

246 

247 return line_list 

248 

249 

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

251 """ 

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

253 

254 Parameters 

255 ---------- 

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

257 The axes on which the velocity data is plotted. 

258 dataset : `M1M3ICSAnalysis` 

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

260 """ 

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

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

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

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

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

266 

267 

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

269 """ 

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

271 

272 Parameters 

273 ---------- 

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

275 The axes on which the torque data is plotted. 

276 dataset : `M1M3ICSAnalysis` 

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

278 """ 

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

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

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

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

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

284 

285 

286def plot_stable_region( 

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

288) -> Optional[Patch]: 

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

290 

291 Parameters 

292 ---------- 

293 fig : `plt.Figure` 

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

295 highlighted. 

296 begin : `astropy.time.Time` 

297 The beginning time of the stable region. 

298 end : `astropy.time.Time` 

299 The ending time of the stable region. 

300 label : `str`, optional 

301 The label for the highlighted region. 

302 color : `str`, optional 

303 The color of the highlighted region. 

304 

305 Returns 

306 ------- 

307 patch : `matplotlib.patches.Patch` 

308 The patch object representing the highlighted region. 

309 """ 

310 span = None # Fixes mypy error about uninitialized variable 

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

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

313 return span 

314 

315 

316def plot_hp_measured_data( 

317 dataset: M1M3ICSAnalysis, 

318 fig: plt.Figure, 

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

320 log: logging.Logger | None = None, 

321) -> Figure: 

322 """ 

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

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

325 

326 Parameters 

327 ---------- 

328 dataset : `M1M3ICSAnalysis` 

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

330 fig : `plt.Figure` 

331 The figure to be plotted on. 

332 commands : `dict`, optional 

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

334 as the command strings themselves. 

335 log : `logging.Logger`, optional 

336 The logger object to log progress. 

337 """ 

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

339 

340 # Start clean 

341 fig.clear() 

342 

343 # Add subplots 

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

345 

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

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

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

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

350 

351 # Remove frame from axis dedicated to label 

352 ax_label.axis("off") 

353 

354 # Plotting 

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

356 for hp in range(dataset.number_of_hardpoints): 

357 topic = dataset.measured_forces_topics[hp] 

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

359 line_list.append(line) 

360 

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

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

363 

364 mark_slew_begin_end(ax_hp, slew_begin, slew_end) 

365 mark_slew_begin_end(ax_vel, slew_begin, slew_end) 

366 line = mark_slew_begin_end(ax_tor, slew_begin, slew_end) 

367 line_list.append(line) 

368 

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

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

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

372 line_list.append(line) 

373 

374 plot_velocity_data(ax_vel, dataset) 

375 plot_torque_data(ax_tor, dataset) 

376 

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

378 colorCounter = 0 

379 if commands is not None: 

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

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

382 

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

384 line = ax.axvline( 

385 commandTime, 

386 c=lineColors[colorCounter], 

387 ls="--", 

388 alpha=0.75, 

389 label=f"{command}", 

390 ) 

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

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

393 

394 customize_hp_plot(ax_hp, line_list) 

395 

396 handles, labels = ax_hp.get_legend_handles_labels() 

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

398 

399 customize_fig(fig, dataset) 

400 

401 return fig