Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 112 additions & 22 deletions python/lsst/summit/extras/slewTimingSimonyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import warnings
from typing import TYPE_CHECKING

import pandas as pd
from astropy.time import TimeDelta
from lsst_efd_client import EfdClient
from matplotlib.lines import Line2D
Expand All @@ -38,8 +39,10 @@
from lsst.utils.plotting.figures import make_figure

if TYPE_CHECKING:
from astropy.time import Time
from matplotlib.figure import Figure


__all__ = ["plotExposureTiming"]

READOUT_TIME = TimeDelta(2.3, format="sec")
Expand Down Expand Up @@ -119,6 +122,8 @@
# 'lsst.sal.MTAOS.logevent_wavefrontError',
# 'lsst.sal.MTAOS.logevent_wepDuration'
# Brian says to find + add the settle event
# MTDome
# "lsst.sal.MTDome.azimuth"
]

HEXAPOD_TOPICS = [
Expand Down Expand Up @@ -163,13 +168,17 @@
"Camera cable wrap": "lsst.sal.MTMount.logevent_cameraCableWrapInPosition",
"Elevation": "lsst.sal.MTMount.logevent_elevationInPosition",
"Rotator": "lsst.sal.MTRotator.logevent_inPosition",
"Dome": "lsst.sal.MTDome.logevent_azMotion",
}


def getAxisName(topic):
# Note the order here matters, e.g. cameraCableWrap is a substring of
# MTMount so it should be checked first, likewise axes are special cases
# of the MTMount so should be checked first.
if "MTDome.logevent_azMotion" in topic:
return "dome"

if "MTMount.logevent_elevationInPosition" in topic:
return "el"

Expand All @@ -189,6 +198,60 @@ def getAxisName(topic):
return "aos"


def getDomeData(
client: EfdClient, begin: Time, end: Time, prePadding: float, postPadding: float, threshold: float = 2.7
) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Get dome data and when dome is within threshold of being in position.

Parameters
----------
client : `EfdClient`
The client object used to retrieve EFD data.
begin : `astropy.time.Time`
The begin time for the data retrieval.
end : `astropy.time.Time`
The end time for the data retrieval.
prePadding : `float`
The amount of time in seconds to pad before the begin time.
postPadding : `float`
The amount of time in seconds to pad after the end time.
threshold : `float`, optional
The threshold in degrees for considering the dome to be in position.

Returns
-------
domeData : `pd.DataFrame`
The dome data with actual and commanded positions.
domeBelowThreshold : `pd.DataFrame`
A dataframe with a single entry indicating the time when the dome
position error drops below the threshold.
"""
domeData = getEfdData(
client,
"lsst.sal.MTDome.azimuth",
columns=["positionActual", "positionCommanded"],
begin=begin,
end=end,
prePadding=prePadding,
postPadding=postPadding,
)
# find the time when the dome position error drops below threshold
domeData["diff"] = (domeData["positionActual"] - domeData["positionCommanded"]).abs()
# Boolean mask where condition holds
mask = domeData["diff"] < threshold
# Rising edge: True when mask is True
# but previous sample was False (or NaN at start)
rising = mask & (~mask.shift(1, fill_value=False))
if rising.any():
# The last rising edge
# (latest time where we enter the < threshold region)
event_time = rising[rising].index.max()

# Make a new dataframe with the domeBelowThreshold
domeBelowThreshold = pd.DataFrame(data={"inPosition": [True]}, index=[event_time])
return domeData, domeBelowThreshold


def plotExposureTiming(
client: EfdClient,
expRecords: list[dafButler.DimensionRecord],
Expand Down Expand Up @@ -217,9 +280,8 @@ def plotExposureTiming(
postPadding : `float`, optional
The amount of time to pad after the end of the last exposure.
narrowHeightRatio : `float`, optional
Height ratio for narrow panels (mount, camera, aos) relative to wide
ones.

Height ratio for narrow panels (mount, dome, camera, aos) relative
to wide ones.
Returns
-------
fig : `matplotlib.figure.Figure` or `None`
Expand Down Expand Up @@ -247,18 +309,21 @@ def plotExposureTiming(
mountData = getAzElRotHexDataForPeriod(client, begin, end, prePadding, postPadding)
if mountData.empty:
log.warning(f"No mount data found for dayObs {dayObs} seqNums {startSeqNum}-{endSeqNum}")
return
return None

az = mountData.azimuthData
el = mountData.elevationData
rot = mountData.rotationData

domeData, domeBelowThreshold = getDomeData(client, begin, end, prePadding, postPadding)

# Calculate relative heights for the gridspec
narrowHeight = narrowHeightRatio
wideHeight = 1.0
totalHeight = 3 * narrowHeight + 3 * wideHeight
totalHeight = 3 * narrowHeight + 4 * wideHeight
heights = [
narrowHeight / totalHeight, # mount
wideHeight / totalHeight, # dome
wideHeight / totalHeight, # azimuth
wideHeight / totalHeight, # elevation
wideHeight / totalHeight, # rotation
Expand All @@ -268,26 +333,29 @@ def plotExposureTiming(

# Create figure with adjusted gridspec
fig = make_figure(figsize=(18, 8))
gs = fig.add_gridspec(7, 2, height_ratios=[*heights, 0.15], width_ratios=[0.8, 0.2], hspace=0)
gs = fig.add_gridspec(8, 2, height_ratios=[*heights, 0.15], width_ratios=[0.8, 0.2], hspace=0)

# Create axes with shared x-axis
mountAx = fig.add_subplot(gs[0, 0])
azimuthAx = fig.add_subplot(gs[1, 0], sharex=mountAx)
elevationAx = fig.add_subplot(gs[2, 0], sharex=mountAx)
rotationAx = fig.add_subplot(gs[3, 0], sharex=mountAx)
aosAx = fig.add_subplot(gs[4, 0], sharex=mountAx)
cameraAx = fig.add_subplot(gs[5, 0], sharex=mountAx)
bottomLegendAx = fig.add_subplot(gs[6, :])
domeAx = fig.add_subplot(gs[1, 0])
azimuthAx = fig.add_subplot(gs[2, 0], sharex=mountAx)
elevationAx = fig.add_subplot(gs[3, 0], sharex=mountAx)
rotationAx = fig.add_subplot(gs[4, 0], sharex=mountAx)
aosAx = fig.add_subplot(gs[5, 0], sharex=mountAx)
cameraAx = fig.add_subplot(gs[6, 0], sharex=mountAx)
bottomLegendAx = fig.add_subplot(gs[7, :])

# Create legend axes
mountLegendAx = fig.add_subplot(gs[0, 1])
azLegendAx = fig.add_subplot(gs[1, 1])
elLegendAx = fig.add_subplot(gs[2, 1])
rotLegendAx = fig.add_subplot(gs[3, 1])
aosLegendAx = fig.add_subplot(gs[4, 1])
cameraLegandAx = fig.add_subplot(gs[5, 1])
domeLegendAx = fig.add_subplot(gs[1, 1])
azLegendAx = fig.add_subplot(gs[2, 1])
elLegendAx = fig.add_subplot(gs[3, 1])
rotLegendAx = fig.add_subplot(gs[4, 1])
aosLegendAx = fig.add_subplot(gs[5, 1])
cameraLegendAx = fig.add_subplot(gs[6, 1])

axes = {
"dome": domeAx,
"az": azimuthAx,
"el": elevationAx,
"rot": rotationAx,
Expand All @@ -297,11 +365,12 @@ def plotExposureTiming(
}

legendAxes = {
"dome": domeLegendAx,
"az": azLegendAx,
"el": elLegendAx,
"rot": rotLegendAx,
"mount": mountLegendAx,
"camera": cameraLegandAx,
"camera": cameraLegendAx,
"aos": aosLegendAx,
"bottom": bottomLegendAx,
}
Expand All @@ -316,7 +385,9 @@ def plotExposureTiming(
axes["az"].plot(az["actualPosition"])
axes["el"].plot(el["actualPosition"])
axes["rot"].plot(rot["actualPosition"])

axes["dome"].plot(domeData["positionActual"], label="Actual")
axes["dome"].plot(domeData["positionCommanded"], label="Commanded")
axes["dome"].legend(loc="lower left", frameon=False)
# Remove y-ticks for mount, aos, and camera axes
for ax_name in ["mount", "aos", "camera"]:
axes[ax_name].set_yticks([])
Expand Down Expand Up @@ -353,7 +424,7 @@ def plotExposureTiming(
)

# Create separate legend entries for each axis type
legendEntries = {ax_name: [] for ax_name in axes.keys()}
legendEntries: dict[str, list] = {ax_name: [] for ax_name in axes.keys()}

# Handle in-position transitions
for label, topic in inPositionTopics.items():
Expand Down Expand Up @@ -395,6 +466,26 @@ def plotExposureTiming(
),
]
)
# Add special domeBelowThreshold axvline
if label == "Dome":
inPositionTransitions = domeBelowThreshold
for time, data in inPositionTransitions.iterrows():
inPosition = data["inPosition"]
if inPosition:
axes[axisName].axvline(time, color="magenta", linestyle="--", alpha=inPositionAlpha)

legendEntries[axisName].extend(
[
Line2D(
[0],
[0],
color="magenta",
linestyle="-",
label=f"{label} below threshold=True",
alpha=inPositionAlpha,
),
]
)

# Handle commands
commandTimes = getCommands(
Expand All @@ -417,14 +508,13 @@ def plotExposureTiming(
log.warning(f"Failed to get data for {topic}")

# Create color maps for each axis
color_maps = {ax_name: {} for ax_name in axes.keys()}
color_maps: dict[str, dict[str, str]] = {ax_name: {} for ax_name in axes.keys()}
colors = ["b", "g", "r", "c", "m", "y", "k"]
color_iterators = {ax_name: itertools.cycle(colors) for ax_name in axes.keys()}

# Group commands by axis and assign colors
for time, command in commandTimes.items():
axisName = getAxisName(command)

if command not in color_maps[axisName]:
color_maps[axisName][command] = next(color_iterators[axisName])
color = color_maps[axisName][command]
Expand Down