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-24 09:02 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 09:02 +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
23import logging
24from typing import TYPE_CHECKING, Any, Optional, TypedDict
26import matplotlib.pyplot as plt
27import pandas as pd
28from astropy.time import Time
29from matplotlib.patches import Patch
31if TYPE_CHECKING:
32 from matplotlib.figure import Figure
34 from ..inertia_compensation_system import M1M3ICSAnalysis
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]
56# Approximate value for breakaway
57HP_BREAKAWAY_LIMIT: float = 3000 # [N]
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]
63# desired operational limit
64HP_OPERATIONAL_LIMIT_COMPRESSION: float = 521 # [N]
65HP_OPERATIONAL_LIMIT_TENSION: float = -581 # [N]
67FIGURE_WIDTH = 10
68FIGURE_HEIGHT = 7
71class HPLimitsDict(TypedDict, total=False):
72 pos_limit: float
73 neg_limit: float
74 ls: str
77def plot_hp_data(ax: plt.Axes, data: pd.Series | list, label: str) -> plt.Line2D:
78 """
79 Plot hardpoint data on the given axes.
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.
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]
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.
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.
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
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.
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.
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
157def customize_fig(fig: plt.Figure, dataset: M1M3ICSAnalysis) -> None:
158 """
159 Add a title to a figure and adjust its subplots spacing
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 )
178 fig.subplots_adjust(hspace=0)
181def customize_hp_plot(ax: plt.Axes, lines: list[plt.Line2D]) -> None:
182 """
183 Customize the appearance of the hardpoint plot.
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)
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)
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.
206 This was first discussed on Slack. From Doug Neil we got:
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.
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 }
239 kwargs: dict[str, Any] = dict(alpha=0.5, lw=1.0, c="r", zorder=-1)
240 line_list = []
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)
247 return line_list
250def plot_velocity_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None:
251 """
252 Plot the azimuth and elevation velocities on the given axes.
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")
268def plot_torque_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None:
269 """
270 Plot the azimuth and elevation torques on the given axes.
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")
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.
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.
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
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`.
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__)
340 # Start clean
341 fig.clear()
343 # Add subplots
344 gs = fig.add_gridspec(4, 1, height_ratios=[1, 2, 1, 1])
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)
351 # Remove frame from axis dedicated to label
352 ax_label.axis("off")
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)
361 slew_begin = Time(dataset.event.begin, scale="utc")
362 slew_end = Time(dataset.event.end, scale="utc")
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)
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)
374 plot_velocity_data(ax_vel, dataset)
375 plot_torque_data(ax_tor, dataset)
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.", "")
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
394 customize_hp_plot(ax_hp, line_list)
396 handles, labels = ax_hp.get_legend_handles_labels()
397 ax_label.legend(handles, labels, loc="center", frameon=False, ncol=4, fontsize="x-small")
399 customize_fig(fig, dataset)
401 return fig