Coverage for python/lsst/summit/utils/m1m3/plots/inertia_compensation_system.py: 19%

98 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-03 04:43 -0700

1import logging 

2 

3import matplotlib.pyplot as plt 

4import pandas as pd 

5from astropy.time import Time 

6 

7from lsst.summit.utils.type_utils import M1M3ICSAnalysis 

8 

9__all__ = [ 

10 "plot_hp_data", 

11 "mark_slew_begin_end", 

12 "mark_padded_slew_begin_end", 

13 "customize_fig", 

14 "customize_hp_plot", 

15 "add_hp_limits", 

16 "plot_velocity_data", 

17 "plot_torque_data", 

18 "plot_stable_region", 

19 "plot_hp_measured_data", 

20 "HP_BREAKAWAY_LIMIT", 

21 "HP_FATIGUE_LIMIT", 

22 "HP_OPERATIONAL_LIMIT", 

23 "FIGURE_WIDTH", 

24 "FIGURE_HEIGHT", 

25] 

26 

27# Approximate value for breakaway 

28HP_BREAKAWAY_LIMIT: float = 3000 # [N] 

29 

30# limit that can still damage the mirror with fatigue 

31HP_FATIGUE_LIMIT: float = 900 # [N] 

32 

33# desired operational limit 

34HP_OPERATIONAL_LIMIT: float = 450 # [N] 

35 

36FIGURE_WIDTH = 10 

37FIGURE_HEIGHT = 7 

38 

39 

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

41 """ 

42 Plot hardpoint data on the given axes. 

43 

44 Parameters 

45 ---------- 

46 ax : `plt.Axes` 

47 The axes on which the data is plotted. 

48 topic : `str` 

49 The topic of the data. 

50 data : `Series` or `list` 

51 The data points to be plotted. 

52 label : `str` 

53 The label for the plotted data. 

54 

55 Returns 

56 ------- 

57 lines : `plt.Line2D` 

58 The plotted data as a Line2D object. 

59 """ 

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

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

62 return line[0] 

63 

64 

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

66 """ 

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

68 axes. 

69 

70 Parameters 

71 ---------- 

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

73 The axes where the vertical lines are drawn. 

74 slew_begin : `astropy.time.Time` 

75 The slew beginning time. 

76 slew_end : `astropy.time.Time` 

77 The slew ending time. 

78 

79 Returns 

80 ------- 

81 line : `matplotlib.lines.Line2D` 

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

83 """ 

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

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

86 return line 

87 

88 

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

90 """ 

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

92 

93 Parameters 

94 ---------- 

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

96 The axes where the vertical lines are drawn. 

97 begin : `astropy.time.Time` 

98 The padded slew beginning time. 

99 end : `astropy.time.Time` 

100 The padded slew ending time. 

101 

102 Returns 

103 ------- 

104 line : `matplotlib.lines.Line2D` 

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

106 """ 

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

108 line = ax.axvline( 

109 end.datetime, 

110 alpha=0.5, 

111 lw=0.5, 

112 ls="-", 

113 c="k", 

114 zorder=-1, 

115 label="Padded Slew Start/Stop", 

116 ) 

117 return line 

118 

119 

120def customize_fig(fig: plt.Figure, dataset: M1M3ICSAnalysis): 

121 """ 

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

123 

124 Paramters 

125 --------- 

126 fig : `matplotlib.pyplot.Figure` 

127 Figure to be custoized. 

128 dataset : `M1M3ICSAnalysis` 

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

130 """ 

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

132 fig.suptitle( 

133 f"HP Measured Data\n " 

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

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

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

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

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

139 ) 

140 

141 fig.subplots_adjust(hspace=0) 

142 

143 

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

145 """ 

146 Customize the appearance of the hardpoint plot. 

147 

148 Parameters 

149 ---------- 

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

151 The axes of the plot to be customized. 

152 lines : `list` 

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

154 """ 

155 limit_lines = add_hp_limits(ax) 

156 lines.extend(limit_lines) 

157 

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

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

160 ax.set_ylim(-3100, 3100) 

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

162 

163 

164def add_hp_limits(ax: plt.Axes): 

165 """ 

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

167 and the operational limits. 

168 

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

170 

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

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

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

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

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

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

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

178 

179 Parameters 

180 ---------- 

181 ax : `plt.Axes` 

182 The axes on which the velocity data is plotted. 

183 """ 

184 hp_limits = { 

185 "HP Breakaway Limit": {"pos_limit": HP_BREAKAWAY_LIMIT, "neg_limit": -HP_BREAKAWAY_LIMIT, "ls": "-"}, 

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

187 "pos_limit": HP_FATIGUE_LIMIT, 

188 "neg_limit": -HP_FATIGUE_LIMIT, 

189 "ls": "--", 

190 }, 

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

192 "pos_limit": HP_OPERATIONAL_LIMIT, 

193 "neg_limit": -HP_OPERATIONAL_LIMIT, 

194 "ls": ":", 

195 }, 

196 } 

197 

198 kwargs = dict(alpha=0.5, lw=1.0, c="r", zorder=-1) 

199 line_list = [] 

200 

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

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

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

204 line_list.append(line) 

205 

206 return line_list 

207 

208 

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

210 """ 

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

212 

213 Parameters 

214 ---------- 

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

216 The axes on which the velocity data is plotted. 

217 dataset : `M1M3ICSAnalysis` 

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

219 """ 

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

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

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

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

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

225 

226 

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

228 """ 

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

230 

231 Parameters 

232 ---------- 

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

234 The axes on which the torque data is plotted. 

235 dataset : `M1M3ICSAnalysis` 

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

237 """ 

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

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

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

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

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

243 

244 

245def plot_stable_region( 

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

247) -> plt.Polygon: 

248 """ 

249 Highlight a stable region on the plot with a colored span. 

250 

251 Parameters 

252 ---------- 

253 fig : `plt.Figure` 

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

255 highlighted. 

256 begin : `astropy.time.Time` 

257 The beginning time of the stable region. 

258 end : `astropy.time.Time` 

259 The ending time of the stable region. 

260 label : `str`, optional 

261 The label for the highlighted region. 

262 color : `str`, optional 

263 The color of the highlighted region. 

264 

265 Returns 

266 ------- 

267 polygon : `matplotlib.patches.Polygon` 

268 The Polygon object representing the highlighted region. 

269 """ 

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

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

272 return span 

273 

274 

275def plot_hp_measured_data( 

276 dataset: M1M3ICSAnalysis, 

277 fig: plt.Figure, 

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

279 log: logging.Logger | None = None, 

280) -> plt.Figure: 

281 """ 

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

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

284 

285 Parameters 

286 ---------- 

287 dataset : `M1M3ICSAnalysis` 

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

289 fig : `plt.Figure` 

290 The figure to be plotted on. 

291 commands : `dict`, optional 

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

293 as the command strings themselves. 

294 log : `logging.Logger`, optional 

295 The logger object to log progress. 

296 """ 

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

298 

299 # Start clean 

300 fig.clear() 

301 

302 # Add subplots 

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

304 

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

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

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

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

309 

310 # Remove frame from axis dedicated to label 

311 ax_label.axis("off") 

312 

313 # Plotting 

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

315 for hp in range(dataset.number_of_hardpoints): 

316 topic = dataset.measured_forces_topics[hp] 

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

318 line_list.append(line) 

319 

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

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

322 

323 mark_slew_begin_end(ax_hp, slew_begin, slew_end) 

324 mark_slew_begin_end(ax_vel, slew_begin, slew_end) 

325 line = mark_slew_begin_end(ax_tor, slew_begin, slew_end) 

326 line_list.append(line) 

327 

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

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

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

331 line_list.append(line) 

332 

333 plot_velocity_data(ax_vel, dataset) 

334 plot_torque_data(ax_tor, dataset) 

335 

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

337 colorCounter = 0 

338 if commands is not None: 

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

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

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

342 line = ax.axvline( 

343 commandTime.utc.datetime, 

344 c=lineColors[colorCounter], 

345 ls="--", 

346 alpha=0.75, 

347 label=f"{command}", 

348 ) 

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

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

351 

352 customize_hp_plot(ax_hp, line_list) 

353 

354 handles, labels = ax_hp.get_legend_handles_labels() 

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

356 

357 customize_fig(fig, dataset) 

358 

359 return fig