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-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/>.
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",
49 "HP_OPERATIONAL_LIMIT",
50 "FIGURE_WIDTH",
51 "FIGURE_HEIGHT",
52]
54# Approximate value for breakaway
55HP_BREAKAWAY_LIMIT: float = 3000 # [N]
57# limit that can still damage the mirror with fatigue
58HP_FATIGUE_LIMIT: float = 900 # [N]
60# desired operational limit
61HP_OPERATIONAL_LIMIT: float = 450 # [N]
63FIGURE_WIDTH = 10
64FIGURE_HEIGHT = 7
67class HPLimitsDict(TypedDict, total=False):
68 pos_limit: float
69 neg_limit: float
70 ls: str
73def plot_hp_data(ax: plt.Axes, data: pd.Series | list, label: str) -> plt.Line2D:
74 """
75 Plot hardpoint data on the given axes.
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.
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]
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.
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.
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
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.
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.
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
153def customize_fig(fig: plt.Figure, dataset: M1M3ICSAnalysis) -> None:
154 """
155 Add a title to a figure and adjust its subplots spacing
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 )
174 fig.subplots_adjust(hspace=0)
177def customize_hp_plot(ax: plt.Axes, lines: list[plt.Line2D]) -> None:
178 """
179 Customize the appearance of the hardpoint plot.
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)
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)
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.
202 This was first discussed on Slack. From Doug Neil we got:
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.
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 }
235 kwargs: dict[str, Any] = dict(alpha=0.5, lw=1.0, c="r", zorder=-1)
236 line_list = []
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)
243 return line_list
246def plot_velocity_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None:
247 """
248 Plot the azimuth and elevation velocities on the given axes.
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")
264def plot_torque_data(ax: plt.Axes, dataset: M1M3ICSAnalysis) -> None:
265 """
266 Plot the azimuth and elevation torques on the given axes.
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")
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.
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.
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
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`.
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__)
336 # Start clean
337 fig.clear()
339 # Add subplots
340 gs = fig.add_gridspec(4, 1, height_ratios=[1, 2, 1, 1])
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)
347 # Remove frame from axis dedicated to label
348 ax_label.axis("off")
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)
357 slew_begin = Time(dataset.event.begin, scale="utc")
358 slew_end = Time(dataset.event.end, scale="utc")
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)
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)
370 plot_velocity_data(ax_vel, dataset)
371 plot_torque_data(ax_tor, dataset)
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.", "")
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
390 customize_hp_plot(ax_hp, line_list)
392 handles, labels = ax_hp.get_legend_handles_labels()
393 ax_label.legend(handles, labels, loc="center", frameon=False, ncol=4, fontsize="x-small")
395 customize_fig(fig, dataset)
397 return fig