Coverage for python / lsst / summit / extras / slewTimingSimonyi.py: 0%
193 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 09:04 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 09:04 +0000
1# This file is part of summit_extras.
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 itertools
24import logging
25import warnings
26from typing import TYPE_CHECKING
28import pandas as pd
29from astropy.time import TimeDelta
30from lsst_efd_client import EfdClient
31from matplotlib.lines import Line2D
32from matplotlib.patches import Patch
34import lsst.daf.butler as dafButler
35import lsst.summit.utils.butlerUtils as butlerUtils
36from lsst.summit.utils.dateTime import dayObsIntToString
37from lsst.summit.utils.efdUtils import getCommands, getEfdData
38from lsst.summit.utils.simonyi.mountData import getAzElRotHexDataForPeriod
39from lsst.utils.plotting.figures import make_figure
41if TYPE_CHECKING:
42 from astropy.time import Time
43 from matplotlib.figure import Figure
46__all__ = ["plotExposureTiming"]
48READOUT_TIME = TimeDelta(2.3, format="sec")
50COMMANDS_TO_QUERY = [
51 # MTPtg
52 "lsst.sal.MTPtg.command_azElTarget",
53 "lsst.sal.MTPtg.command_disable",
54 "lsst.sal.MTPtg.command_enable",
55 "lsst.sal.MTPtg.command_exitControl",
56 "lsst.sal.MTPtg.command_offsetAzEl",
57 "lsst.sal.MTPtg.command_poriginOffset",
58 "lsst.sal.MTPtg.command_raDecTarget",
59 "lsst.sal.MTPtg.command_rotOffset",
60 "lsst.sal.MTPtg.command_standby",
61 "lsst.sal.MTPtg.command_start",
62 "lsst.sal.MTPtg.command_startTracking",
63 "lsst.sal.MTPtg.command_stopTracking",
64 # MTMount
65 "lsst.sal.MTMount.command_applySettingsSet",
66 "lsst.sal.MTMount.command_closeMirrorCovers",
67 "lsst.sal.MTMount.command_disable",
68 "lsst.sal.MTMount.command_enable",
69 "lsst.sal.MTMount.command_enableCameraCableWrapFollowing",
70 "lsst.sal.MTMount.command_exitControl",
71 "lsst.sal.MTMount.command_homeBothAxes",
72 "lsst.sal.MTMount.command_moveToTarget",
73 "lsst.sal.MTMount.command_openMirrorCovers",
74 "lsst.sal.MTMount.command_park",
75 "lsst.sal.MTMount.command_restoreDefaultSettings",
76 "lsst.sal.MTMount.command_setLogLevel",
77 "lsst.sal.MTMount.command_standby",
78 "lsst.sal.MTMount.command_start",
79 "lsst.sal.MTMount.command_startTracking",
80 "lsst.sal.MTMount.command_stop",
81 "lsst.sal.MTMount.command_stopTracking",
82 # 'lsst.sal.MTMount.command_trackTarget', # exclude the 20Hz data
83 # M1M3
84 "lsst.sal.MTM1M3.command_clearSlewFlag",
85 "lsst.sal.MTM1M3.command_setSlewControllerSettings",
86 "lsst.sal.MTM1M3.command_setSlewFlag",
87 "lsst.sal.MTM1M3.logevent_slewControllerSettings"
88 # MTCamera
89 "lsst.sal.MTCamera.logevent_startIntegration",
90 "lsst.sal.MTCamera.logevent_startLoadFilter",
91 "lsst.sal.MTCamera.logevent_startReadout",
92 "lsst.sal.MTCamera.logevent_startRotateCarousel",
93 "lsst.sal.MTCamera.logevent_startSetFilter",
94 "lsst.sal.MTCamera.logevent_startShutterClose",
95 "lsst.sal.MTCamera.logevent_startShutterOpen",
96 "lsst.sal.MTCamera.logevent_startUnloadFilter",
97 "lsst.sal.MTCamera.logevent_endLoadFilter",
98 "lsst.sal.MTCamera.logevent_endOfImageTelemetry",
99 "lsst.sal.MTCamera.logevent_endReadout",
100 "lsst.sal.MTCamera.logevent_endRotateCarousel",
101 "lsst.sal.MTCamera.logevent_endSetFilter",
102 "lsst.sal.MTCamera.logevent_endShutterClose",
103 "lsst.sal.MTCamera.logevent_endShutterOpen",
104 "lsst.sal.MTCamera.logevent_endUnloadFilter",
105 # MTAos
106 # 'lsst.sal.MTAOS.logevent_cameraHexapodCorrection',
107 "lsst.sal.MTAOS.logevent_configurationApplied",
108 # 'lsst.sal.MTAOS.logevent_configurationsAvailable',
109 # 'lsst.sal.MTAOS.logevent_degreeOfFreedom',
110 # 'lsst.sal.MTAOS.logevent_errorCode',
111 "lsst.sal.MTAOS.logevent_m1m3Correction",
112 "lsst.sal.MTAOS.logevent_m2Correction",
113 "lsst.sal.MTAOS.logevent_m2HexapodCorrection",
114 # 'lsst.sal.MTAOS.logevent_mirrorStresses',
115 # 'lsst.sal.MTAOS.logevent_ofcDuration',
116 "lsst.sal.MTAOS.logevent_rejectedCameraHexapodCorrection",
117 "lsst.sal.MTAOS.logevent_rejectedDegreeOfFreedom",
118 "lsst.sal.MTAOS.logevent_rejectedM1M3Correction",
119 "lsst.sal.MTAOS.logevent_rejectedM2Correction",
120 "lsst.sal.MTAOS.logevent_rejectedM2HexapodCorrection",
121 # 'lsst.sal.MTAOS.logevent_summaryState',
122 # 'lsst.sal.MTAOS.logevent_wavefrontError',
123 # 'lsst.sal.MTAOS.logevent_wepDuration'
124 # Brian says to find + add the settle event
125 # MTDome
126 # "lsst.sal.MTDome.azimuth"
127]
129HEXAPOD_TOPICS = [
130 "lsst.sal.MTHexapod.ackcmd",
131 # 'lsst.sal.MTHexapod.actuators',
132 # 'lsst.sal.MTHexapod.application',
133 "lsst.sal.MTHexapod.command_disable",
134 "lsst.sal.MTHexapod.command_enable",
135 # 'lsst.sal.MTHexapod.command_exitControl',
136 "lsst.sal.MTHexapod.command_move",
137 "lsst.sal.MTHexapod.command_offset",
138 "lsst.sal.MTHexapod.command_setCompensationMode",
139 # 'lsst.sal.MTHexapod.command_setLogLevel',
140 "lsst.sal.MTHexapod.command_standby",
141 "lsst.sal.MTHexapod.command_start",
142 # 'lsst.sal.MTHexapod.electrical',
143 # 'lsst.sal.MTHexapod.logevent_commandableByDDS',
144 # 'lsst.sal.MTHexapod.logevent_compensatedPosition',
145 "lsst.sal.MTHexapod.logevent_compensationMode",
146 # 'lsst.sal.MTHexapod.logevent_compensationOffset',
147 # 'lsst.sal.MTHexapod.logevent_configuration',
148 "lsst.sal.MTHexapod.logevent_configurationApplied",
149 # 'lsst.sal.MTHexapod.logevent_configurationsAvailable',
150 # 'lsst.sal.MTHexapod.logevent_connected',
151 # 'lsst.sal.MTHexapod.logevent_controllerState',
152 "lsst.sal.MTHexapod.logevent_errorCode",
153 # 'lsst.sal.MTHexapod.logevent_heartbeat',
154 "lsst.sal.MTHexapod.logevent_inPosition",
155 # 'lsst.sal.MTHexapod.logevent_interlock',
156 # 'lsst.sal.MTHexapod.logevent_logLevel',
157 # 'lsst.sal.MTHexapod.logevent_logMessage',
158 # 'lsst.sal.MTHexapod.logevent_simulationMode',
159 # 'lsst.sal.MTHexapod.logevent_softwareVersions',
160 # 'lsst.sal.MTHexapod.logevent_summaryState',
161 # 'lsst.sal.MTHexapod.logevent_uncompensatedPosition'
162]
164inPositionTopics = {
165 "Hexapod": "lsst.sal.MTHexapod.logevent_inPosition",
166 "M2": "lsst.sal.MTM2.logevent_m2AssemblyInPosition",
167 "Azimuth": "lsst.sal.MTMount.logevent_azimuthInPosition",
168 "Camera cable wrap": "lsst.sal.MTMount.logevent_cameraCableWrapInPosition",
169 "Elevation": "lsst.sal.MTMount.logevent_elevationInPosition",
170 "Rotator": "lsst.sal.MTRotator.logevent_inPosition",
171 "Dome": "lsst.sal.MTDome.logevent_azMotion",
172}
175def getAxisName(topic):
176 # Note the order here matters, e.g. cameraCableWrap is a substring of
177 # MTMount so it should be checked first, likewise axes are special cases
178 # of the MTMount so should be checked first.
179 if "MTDome.logevent_azMotion" in topic:
180 return "dome"
182 if "MTMount.logevent_elevationInPosition" in topic:
183 return "el"
185 if "MTMount.logevent_azimuthInPosition" in topic:
186 return "az"
188 if "MTRotator.logevent_inPosition" in topic:
189 return "rot"
191 if any(x in topic for x in ["MTCamera", "MTRotator", "cameraCableWrap"]):
192 return "camera"
194 if any(x in topic for x in ["MTPtg", "MTMount", "MTM1M3", "MTM2"]):
195 return "mount"
197 if any(x in topic for x in ["MTAOS", "MTHexapod", "MTM1M3", "MTM2"]):
198 return "aos"
201def getDomeData(
202 client: EfdClient, begin: Time, end: Time, prePadding: float, postPadding: float
203) -> tuple[pd.DataFrame, pd.DataFrame]:
204 """Get dome data and when dome is within threshold of being in position.
206 Parameters
207 ----------
208 client : `EfdClient`
209 The client object used to retrieve EFD data.
210 begin : `astropy.time.Time`
211 The begin time for the data retrieval.
212 end : `astropy.time.Time`
213 The end time for the data retrieval.
214 prePadding : `float`
215 The amount of time in seconds to pad before the begin time.
216 postPadding : `float`
217 The amount of time in seconds to pad after the end time.
219 Returns
220 -------
221 domeData : `pd.DataFrame`
222 The dome data with actual and commanded positions.
223 domeVignetted : `pd.DataFrame`
224 A dataframe with an entry indicating when the telescope
225 is no longer vignetted
226 """
227 domeData = getEfdData(
228 client,
229 "lsst.sal.MTDome.azimuth",
230 columns=["positionActual", "positionCommanded"],
231 begin=begin,
232 end=end,
233 prePadding=prePadding,
234 postPadding=postPadding,
235 )
236 # Get the data with the vignetted flag
237 vignettedData = getEfdData(
238 client,
239 "lsst.sal.MTDomeTrajectory.logevent_telescopeVignetted",
240 columns=["vignetted"],
241 begin=begin,
242 end=end,
243 prePadding=prePadding,
244 postPadding=postPadding,
245 )
246 if len(vignettedData) == 0:
247 vignettedTime = begin.utc.to_datetime()
248 # check we have the column and that it contains at least 1 value of 1
249 if "vignetted" in vignettedData.columns and (vignettedData["vignetted"] == 1).any():
250 vignettedData = vignettedData[vignettedData["vignetted"] == 1]
251 vignettedTime = vignettedData.index[-1]
252 else:
253 log = logging.getLogger(__name__)
254 log.warning("No vignetted data found for the given time range. Assuming always vignetted.")
255 vignettedTime = begin.utc.to_datetime()
256 # Make a new dataframe with the vignetting data
257 domeVignetted = pd.DataFrame(data={"vignetted": [False]}, index=[vignettedTime])
258 return domeData, domeVignetted
261def plotExposureTiming(
262 client: EfdClient,
263 expRecords: list[dafButler.DimensionRecord],
264 prePadding: float = 1,
265 postPadding: float = 3,
266 narrowHeightRatio: float = 0.4,
267 figure: Figure | None = None,
268) -> Figure | None:
269 """Plot the mount command timings for a set of exposures.
271 This function plots the mount position data for the entire time range of
272 the exposures, regardless of whether the exposures are contiguous or not.
273 The exposures are shaded in the plot to indicate the time range for each
274 integration its readout, and any commands issued during the time range are
275 plotted as vertical lines.
277 Parameters
278 ----------
279 client : `EfdClient`
280 The client object used to retrieve EFD data.
281 expRecords : `list` of `lsst.daf.butler.DimensionRecord`
282 A list of exposure records to plot. The timings will be plotted from
283 the start of the first exposure to the end of the last exposure,
284 regardless of whether intermediate exposures are included.
285 prePadding : `float`, optional
286 The amount of time to pad before the start of the first exposure.
287 postPadding : `float`, optional
288 The amount of time to pad after the end of the last exposure.
289 narrowHeightRatio : `float`, optional
290 Height ratio for narrow panels (mount, dome, camera, aos) relative
291 to wide ones.
292 Returns
293 -------
294 fig : `matplotlib.figure.Figure` or `None`
295 The figure containing the plot, or `None` if no data is found.
296 """
297 log = logging.getLogger(__name__)
299 inPositionAlpha = 0.5
300 commandAlpha = 0.5
301 integrationColor = "grey"
302 readoutColor = "blue"
304 expRecords.sort(key=lambda x: (x.day_obs, x.seq_num)) # ensure we're sorted
306 startSeqNum = expRecords[0].seq_num
307 endSeqNum = expRecords[-1].seq_num
308 dayObs = expRecords[0].day_obs
309 if expRecords[-1].day_obs != dayObs:
310 raise ValueError("All exposures must be from the same day_obs")
311 title = f"Mount command timings for {dayObsIntToString(dayObs)} seqNums {startSeqNum} - {endSeqNum}"
313 begin = expRecords[0].timespan.begin
314 end = expRecords[-1].timespan.end
316 mountData = getAzElRotHexDataForPeriod(client, begin, end, prePadding, postPadding)
317 if mountData.empty:
318 log.warning(f"No mount data found for dayObs {dayObs} seqNums {startSeqNum}-{endSeqNum}")
319 return None
321 az = mountData.azimuthData
322 el = mountData.elevationData
323 rot = mountData.rotationData
325 domeData, domeVignetted = getDomeData(client, begin, end, prePadding, postPadding)
327 # Calculate relative heights for the gridspec
328 narrowHeight = narrowHeightRatio
329 wideHeight = 1.0
330 totalHeight = 3 * narrowHeight + 4 * wideHeight
331 heights = [
332 narrowHeight / totalHeight, # mount
333 wideHeight / totalHeight, # dome
334 wideHeight / totalHeight, # azimuth
335 wideHeight / totalHeight, # elevation
336 wideHeight / totalHeight, # rotation
337 narrowHeight / totalHeight, # aos
338 narrowHeight / totalHeight, # camera
339 ]
341 # Create figure with adjusted gridspec
342 figsize = (18, 8)
343 if figure is None:
344 fig = make_figure(figsize=figsize)
345 else:
346 fig = figure
347 log.warning(f"Using provided figure, (recommened {figsize=}). This better be ~in a notebook!")
348 gs = fig.add_gridspec(8, 2, height_ratios=[*heights, 0.15], width_ratios=[0.8, 0.2], hspace=0)
350 # Create axes with shared x-axis
351 mountAx = fig.add_subplot(gs[0, 0])
352 domeAx = fig.add_subplot(gs[1, 0])
353 azimuthAx = fig.add_subplot(gs[2, 0], sharex=mountAx)
354 elevationAx = fig.add_subplot(gs[3, 0], sharex=mountAx)
355 rotationAx = fig.add_subplot(gs[4, 0], sharex=mountAx)
356 aosAx = fig.add_subplot(gs[5, 0], sharex=mountAx)
357 cameraAx = fig.add_subplot(gs[6, 0], sharex=mountAx)
358 bottomLegendAx = fig.add_subplot(gs[7, :])
360 # Create legend axes
361 mountLegendAx = fig.add_subplot(gs[0, 1])
362 domeLegendAx = fig.add_subplot(gs[1, 1])
363 azLegendAx = fig.add_subplot(gs[2, 1])
364 elLegendAx = fig.add_subplot(gs[3, 1])
365 rotLegendAx = fig.add_subplot(gs[4, 1])
366 aosLegendAx = fig.add_subplot(gs[5, 1])
367 cameraLegendAx = fig.add_subplot(gs[6, 1])
369 axes = {
370 "dome": domeAx,
371 "az": azimuthAx,
372 "el": elevationAx,
373 "rot": rotationAx,
374 "mount": mountAx,
375 "camera": cameraAx,
376 "aos": aosAx,
377 }
379 legendAxes = {
380 "dome": domeLegendAx,
381 "az": azLegendAx,
382 "el": elLegendAx,
383 "rot": rotLegendAx,
384 "mount": mountLegendAx,
385 "camera": cameraLegendAx,
386 "aos": aosLegendAx,
387 "bottom": bottomLegendAx,
388 }
390 # Remove frames and ticks from legend axes
391 for ax in legendAxes.values():
392 ax.set_frame_on(False)
393 ax.set_xticks([])
394 ax.set_yticks([])
396 # Plot telemetry
397 axes["az"].plot(az["actualPosition"])
398 axes["el"].plot(el["actualPosition"])
399 axes["rot"].plot(rot["actualPosition"])
400 axes["dome"].plot(domeData["positionActual"], label="Actual")
401 axes["dome"].plot(domeData["positionCommanded"], label="Commanded")
402 axes["dome"].legend(loc="lower left", frameon=False)
403 # Remove y-ticks for mount, aos, and camera axes
404 for ax_name in ["mount", "aos", "camera"]:
405 axes[ax_name].set_yticks([])
407 # Hide the last x-tick label for all axes because although they're shared
408 # the last values sticks out to the right
409 with warnings.catch_warnings():
410 warnings.simplefilter("ignore", UserWarning)
411 for ax in axes.values():
412 ax.set_xticklabels(ax.get_xticklabels()[:-1])
414 # Shade exposure regions and add annotations
415 for record in expRecords:
416 startExposing = record.timespan.begin.utc.datetime
417 endExposing = record.timespan.end.utc.datetime
418 readoutEnd = (record.timespan.end + READOUT_TIME).utc.datetime
419 seqNum = record.seq_num
421 for ax in axes.values():
422 ax.axvspan(startExposing, endExposing, color=integrationColor, alpha=0.3)
423 ax.axvspan(endExposing, readoutEnd, color=readoutColor, alpha=0.1)
425 # Add expRecord details inside the camera section of the plot
426 midpoint = startExposing + (endExposing - startExposing) / 2
427 label = f"seqNum = {seqNum}\nFilter={record.physical_filter}"
428 axes["camera"].annotate(
429 label,
430 xy=(midpoint, 0.2),
431 xycoords=("data", "axes fraction"),
432 ha="center",
433 va="bottom",
434 fontsize=10,
435 color="black",
436 )
438 # Create separate legend entries for each axis type
439 legendEntries: dict[str, list] = {ax_name: [] for ax_name in axes.keys()}
441 # Handle in-position transitions
442 for label, topic in inPositionTopics.items():
443 axisName = getAxisName(topic)
445 inPositionTransitions = getEfdData(
446 client,
447 topic,
448 begin=begin,
449 end=end,
450 prePadding=prePadding,
451 postPadding=postPadding,
452 warn=False,
453 )
454 for time, data in inPositionTransitions.iterrows():
455 inPosition = data["inPosition"]
456 if inPosition:
457 axes[axisName].axvline(time, color="green", linestyle="--", alpha=inPositionAlpha)
458 else:
459 axes[axisName].axvline(time, color="red", linestyle="-", alpha=inPositionAlpha)
461 legendEntries[axisName].extend(
462 [
463 Line2D(
464 [0],
465 [0],
466 color="green",
467 linestyle="--",
468 label=f"{label} in position=True",
469 alpha=inPositionAlpha,
470 ),
471 Line2D(
472 [0],
473 [0],
474 color="red",
475 linestyle="-",
476 label=f"{label} in position=False",
477 alpha=inPositionAlpha,
478 ),
479 ]
480 )
481 # Add special domeVignetted axvline
482 if label == "Dome":
483 vignettingTransitions = domeVignetted
484 for time, data in vignettingTransitions.iterrows():
485 vignetted = data["vignetted"]
486 if not vignetted:
487 axes[axisName].axvline(time, color="magenta", linestyle="--", alpha=inPositionAlpha)
489 legendEntries[axisName].extend(
490 [
491 Line2D(
492 [0],
493 [0],
494 color="magenta",
495 linestyle="-",
496 label=f"{label} vignetted=False",
497 alpha=inPositionAlpha,
498 ),
499 ]
500 )
502 # Handle commands
503 commandTimes = getCommands(
504 client, COMMANDS_TO_QUERY, begin, end, prePadding, postPadding, timeFormat="python"
505 )
507 for topic in HEXAPOD_TOPICS:
508 try:
509 hexData = getEfdData(
510 client,
511 topic,
512 begin=begin,
513 end=end,
514 prePadding=prePadding,
515 postPadding=postPadding,
516 warn=False,
517 )
518 commandTimes.update({time: topic for time, _ in hexData.iterrows()})
519 except ValueError:
520 log.warning(f"Failed to get data for {topic}")
522 # Create color maps for each axis
523 color_maps: dict[str, dict[str, str]] = {ax_name: {} for ax_name in axes.keys()}
524 colors = ["b", "g", "r", "c", "m", "y", "k"]
525 color_iterators = {ax_name: itertools.cycle(colors) for ax_name in axes.keys()}
527 # Group commands by axis and assign colors
528 for time, command in commandTimes.items():
529 axisName = getAxisName(command)
530 if command not in color_maps[axisName]:
531 color_maps[axisName][command] = next(color_iterators[axisName])
532 color = color_maps[axisName][command]
533 axes[axisName].axvline(time, linestyle="-.", alpha=commandAlpha, color=color)
535 # Add to legend entries if not already there
536 shortCommand = command.replace("lsst.sal.", "")
537 if shortCommand not in [entry.get_label() for entry in legendEntries[axisName]]:
538 entry = Line2D([0], [0], color=color, linestyle="-.", label=shortCommand, alpha=commandAlpha)
539 legendEntries[axisName].append(entry)
541 # Create separate legends, using 2 columns if more than 5 items
542 for axisName, entries in legendEntries.items():
543 if entries:
544 ncols = 2 if len(entries) > 5 else 1
545 legendAxes[axisName].legend(
546 handles=entries, loc="center left", bbox_to_anchor=(-0.5, 0.5), ncol=ncols
547 )
549 # Create bottom legend for shading explanation
550 shadingLegendHandles = [
551 Patch(facecolor=integrationColor, alpha=0.3, label="Shutter open period"),
552 Patch(facecolor=readoutColor, alpha=0.1, label="Readout period"),
553 ]
554 bottomLegendAx.legend(handles=shadingLegendHandles, loc="center", bbox_to_anchor=(0.4, 0.5), ncol=2)
556 # For some reason xlim is different on the Dome plot.
557 # This sets it to match:
558 az_xlim = axes["az"].get_xlim()
559 axes["dome"].set_xlim(az_xlim)
561 # Set labels with horizontal orientation
562 for axisName, ax in axes.items():
563 ax.set_ylabel(
564 (
565 f"{axisName.title() if axisName != 'aos' else 'AOS'} commands"
566 if axisName in ["mount", "camera", "aos"]
567 else f"{axisName.title()} (deg)"
568 ),
569 rotation=0,
570 ha="right",
571 va="center",
572 )
573 axes["rot"].set_xlabel("Time (UTC)")
575 # Add title centered on main plot area only
576 axes["mount"].set_title(title)
578 fig.tight_layout()
580 return fig
583if __name__ == "__main__":
584 # example usage
585 import lsst.summit.utils.butlerUtils as butlerUtils # noqa: F811
586 from lsst.summit.extras.slewTimingSimonyi import plotExposureTiming # noqa: F811
587 from lsst.summit.utils.efdUtils import makeEfdClient
589 client = makeEfdClient()
590 butler = butlerUtils.makeDefaultButler("LSSTComCam")
591 dayObs = 20241116
592 where = f"exposure.day_obs={dayObs} AND instrument='LSSTComCam'"
593 records = list(butler.registry.queryDimensionRecords("exposure", where=where))
594 records = sorted(records, key=lambda x: (x.day_obs, x.seq_num))
595 print(f"Found {len(records)} records from {len(set(r.day_obs for r in records))} days")
596 if len(set(r.day_obs for r in records)) == 1:
597 rd = {r.seq_num: r for r in records if r.seq_num >= 1}
598 print(f"{len(rd)} items in the dict")
600 toPlot = [records[98], records[99]]
602 az = plotExposureTiming(client, toPlot)