diff --git a/services/plots/current/app/uv.lock b/services/plots/current/app/uv.lock
index 1909c3c..ecd97cf 100644
--- a/services/plots/current/app/uv.lock
+++ b/services/plots/current/app/uv.lock
@@ -30,7 +30,6 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "agebd" },
- { name = "elogpy" },
{ name = "matplotlib" },
{ name = "urllib3" },
]
@@ -45,7 +44,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "agebd", editable = "../../../../packages/agebd" },
- { name = "elogpy", specifier = ">=1.0.0" },
{ name = "matplotlib", specifier = ">=3.10.9" },
{ name = "urllib3", specifier = ">=2.7.0" },
]
@@ -108,15 +106,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
-[[package]]
-name = "elogpy"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f4/2d/1ceedb78bc28f9b060879b49c45fcb76f71f7f6ac812d38f2794fc4d5303/elogpy-1.0.0.tar.gz", hash = "sha256:9676c108cccdee7c7ab96f1fa850d9ade291e186f70479883d9414cb64c55438", size = 5289, upload-time = "2025-08-26T05:31:24.986Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/9a/b65bb5f9c3a7fb0fc54b9689cd587228d1cc890f31157e55236987fb9205/elogpy-1.0.0-py3-none-any.whl", hash = "sha256:cec1a8fb0e69ac613f282ba2988fb970063358c78543166f79412b38f2bb77fe", size = 5386, upload-time = "2025-08-26T05:31:23.719Z" },
-]
-
[[package]]
name = "exceptiongroup"
version = "1.3.1"
diff --git a/services/postmortemlog/current/app/pyproject.toml b/services/postmortemlog/current/app/pyproject.toml
index dc6c4c3..d2dc249 100644
--- a/services/postmortemlog/current/app/pyproject.toml
+++ b/services/postmortemlog/current/app/pyproject.toml
@@ -5,6 +5,9 @@ description = "AGEBD-POSTMORTEMLOG Service"
requires-python = "==3.10.*"
dependencies = [
"agebd",
+ "matplotlib>=3.10.9",
+ "numpy>=2.0.2",
+ "urllib3>=2.7.0",
]
[dependency-groups]
diff --git a/services/postmortemlog/current/app/src/agebd_postmortemlog/service.py b/services/postmortemlog/current/app/src/agebd_postmortemlog/service.py
index f0d50db..5c05f25 100644
--- a/services/postmortemlog/current/app/src/agebd_postmortemlog/service.py
+++ b/services/postmortemlog/current/app/src/agebd_postmortemlog/service.py
@@ -1,49 +1,515 @@
import time
-from epics import dbr
+# https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings
+import urllib3
-from agebd.pv import LocalPVLink
+urllib3.disable_warnings()
+import socket
+from datetime import datetime
+from time import perf_counter, sleep, time
+
+import elog
+import matplotlib.pyplot as plt
+import numpy as np
+from epics import poll
+
+from agebd.h5 import h5save
+from agebd.pv import get_pv_class
from agebd.service.base import BaseService
from agebd.service.pvs import BasePVs
from agebd.utils import printgetversion
-from agebd.pv import get_pv_class
__version__ = printgetversion(__file__)
PV = get_pv_class()
+plt.switch_backend("agg")
+plt.rcParams.update({"font.size": 18})
+plt.rcParams["axes.facecolor"] = "white"
+plt.rcParams["figure.facecolor"] = "white"
+plt.rcParams["lines.markersize"] = 1
+plt.rcParams["lines.linewidth"] = 0.2
+
+EpicsTimestampStr2FileTimestampStr = lambda ts: datetime.strptime(
+ ts, "%Y-%m-%d %H:%M:%S.%f"
+).strftime("%Y%m%d_%H%M%S")
+
class PVs(BasePVs):
- # Option 1:
- ## Service specific PVs from dedicated IOC
- my_pv1 = PV("AGEBD-POSTMORTEMLOG:AO") # TODO: is this a good PV to use?
+ # # Generic Service Control PV
+ test = PV("AGEBD-POSTMORTEMLOG:TEST")
+ postdelay = PV("AGEBD-POSTMORTEMLOG:BEAM-ABORT-EVT.ODLY")
- ## Service specific PVs from other IOCs
- # ...
+ # Potential Service Callback PVs
+ bdcinputsum = PV("AGESA-SBDC-0000:SUM_INPUT_NOT_OK.VAL", auto_monitor=True)
+ bdcinputsum_latched = PV("AGESA-SBDC-0000:SUM_INPUT_NOT_OK_LATCHED.VAL", auto_monitor=True)
+
+ # Minimum current condition
+ pctmaster = PV("ARS07-DPCT-0000:CURR.VAL")
+
+ # Service Input PVs (Post Mortem Information)
+ bdc_interlocks = [
+ PV("AGESA-SBDC-0000:CH{:02.0f}-INPUT_OK_LATCHED.VAL".format(i)) for i in range(21)
+ ]
+ bdc_delays = [PV("AGESA-SBDC-0000:CH{:02.0f}-DELAY.VAL".format(i)) for i in range(21)]
+
+ pmioc_time = PV("ARS01-MKIK-0050-1:TRIG-CNT.VAL")
+
+ vcs_alarmlist = PV("ARS00-VCS:PLC_AlarmList.VAL")
+ mis_alarmlist = PV("ARS00-MIS-PLC-01:AlarmList.VAL")
+ misfe_alarmlist = PV("ARS00-MIS-PLC-01:AlarmList_Frontends.VAL")
+
+ # Counters
+ counter_bdc = PV("AGESA-SBDC-0000:INT-COUNTER.VAL", auto_monitor=True)
+ counter_dumpkicker = PV("ARS01-MKIK-0050-1:TRIG-CNT.VAL")
+ counter_tmaster_beamdump = PV("AGETI-CVME-MASTER-TMA:Evt-Beam-Dump-EVRCount-I.VAL")
+ counter_tmaster_postmortem = PV("AGETI-CVME-MASTER-TMA:Evt-PM-Rec-EVRCount-I.VAL")
+
+ # Post Mortem Data as dict for easier access
+ PostMortemLinks = [
+ ## BLMs
+ # IOC: ABODI-DFBL
+ PV("ABODI-DFBL:SigPm.A"),
+ PV("ABODI-DFBL:SigPm.B"),
+ PV("ABODI-DFBL:SigPm.C"),
+ PV("ABODI-DFBL:SigPm.D"),
+ # IOC: ARS01-DFBL
+ PV("ARS01-DFBL:SigPm.A"),
+ PV("ARS01-DFBL:SigPm.B"),
+ PV("ARS01-DFBL:SigPm.C"),
+ PV("ARS01-DFBL:SigPm.D"),
+ # IOC: ARS02-DFBL
+ PV("ARS02-DFBL:SigPm.A"),
+ PV("ARS02-DFBL:SigPm.B"),
+ PV("ARS02-DFBL:SigPm.C"),
+ PV("ARS02-DFBL:SigPm.D"),
+ # IOC: ARS06-DFBL
+ PV("ARS06-DFBL:SigPm.A"),
+ PV("ARS06-DFBL:SigPm.B"),
+ PV("ARS06-DFBL:SigPm.C"),
+ PV("ARS06-DFBL:SigPm.D"),
+ # IOC: ARS09-DFBL
+ PV("ARS09-DFBL:SigPm.A"),
+ PV("ARS09-DFBL:SigPm.B"),
+ PV("ARS09-DFBL:SigPm.C"),
+ PV("ARS09-DFBL:SigPm.D"),
+ ## 4 Kicker Bump
+ PV("ARS01-MKIK-0140:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0140:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0220:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0220:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0850:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0850:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0940:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0940:CURRENT-WF.VAL"),
+ ## Beam Dump Kicker
+ PV("ARS01-MKIK-0050-1:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0050-1:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0050-2:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0050-2:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0050-3:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0050-3:CURRENT-WF.VAL"),
+ PV("ARS01-MKIK-0050-4:TIME-AXIS.VAL"),
+ PV("ARS01-MKIK-0050-4:CURRENT-WF.VAL"),
+ ## Beam Dump Post Mortem Digitizer
+ PV("ARF-LPD64:X-AXIS-CH1.VAL"),
+ PV("ARF-LPD64:CH1-DATA.VAL"),
+ PV("ARF-LPD64:X-AXIS-CH2.VAL"),
+ PV("ARF-LPD64:CH2-DATA.VAL"),
+ PV("ARF-LPD64:X-AXIS-CH3.VAL"),
+ PV("ARF-LPD64:CH3-DATA.VAL"),
+ PV("ARF-LPD64:X-AXIS-CH4.VAL"),
+ PV("ARF-LPD64:CH4-DATA.VAL"),
+ PV("AGESA-SBDC-0000:BUNCH0_MAGNET_RAMP_DELAY.VAL"),
+ # ---------- FILLING PATTERN --------------
+ PV("AGEBD-POSTMORTEMLOG:BUFFER-FILLPATTERN-APD.VAL"),
+ ]
+ # ---------- BPM Post Mortem Data --------------
+ bpmnames = [
+ "ARS01-DBPM-0390:",
+ "ARS01-DBPM-0760:",
+ "ARS01-DBPM-1080:",
+ "ARS01-DBPM-1460:",
+ "ARS01-DBPM-1900:",
+ "ARS01-DBPM-2410:",
+ "ARS01-DBPM-2910:",
+ "ARS01-DBPM-4220:",
+ "ARS01-DBPM-4720:",
+ "ARS01-DBPM-5250:",
+ "ARS01-DBPM-5530:",
+ "ARS01-DBPM-5880:",
+ "ARS02-DBPM-1080:",
+ "ARS02-DBPM-1460:",
+ "ARS02-DBPM-1900:",
+ "ARS02-DBPM-2410:",
+ "ARS02-DBPM-2910:",
+ "ARS02-DBPM-4220:",
+ "ARS02-DBPM-4720:",
+ "ARS02-DBPM-5250:",
+ "ARS02-DBPM-5530:",
+ "ARS02-DBPM-5880:",
+ "ARS03-DBPM-0810:",
+ "ARS03-DBPM-1080:",
+ "ARS03-DBPM-1460:",
+ "ARS03-DBPM-1900:",
+ "ARS03-DBPM-2410:",
+ "ARS03-DBPM-2910:",
+ "ARS03-DBPM-4220:",
+ "ARS03-DBPM-4720:",
+ "ARS03-DBPM-5250:",
+ "ARS03-DBPM-5530:",
+ "ARS03-DBPM-5880:",
+ "ARS04-DBPM-0090:",
+ "ARS04-DBPM-0900:",
+ "ARS04-DBPM-1080:",
+ "ARS04-DBPM-1460:",
+ "ARS04-DBPM-1900:",
+ "ARS04-DBPM-2410:",
+ "ARS04-DBPM-2910:",
+ "ARS04-DBPM-4220:",
+ "ARS04-DBPM-4720:",
+ "ARS04-DBPM-5250:",
+ "ARS04-DBPM-5530:",
+ "ARS04-DBPM-5880:",
+ "ARS05-DBPM-0410:",
+ "ARS05-DBPM-1080:",
+ "ARS05-DBPM-1460:",
+ "ARS05-DBPM-1900:",
+ "ARS05-DBPM-2410:",
+ "ARS05-DBPM-2910:",
+ "ARS05-DBPM-4220:",
+ "ARS05-DBPM-4720:",
+ "ARS05-DBPM-5250:",
+ "ARS05-DBPM-5530:",
+ "ARS05-DBPM-5880:",
+ "ARS06-DBPM-1080:",
+ "ARS06-DBPM-1460:",
+ "ARS06-DBPM-1900:",
+ "ARS06-DBPM-2410:",
+ "ARS06-DBPM-2910:",
+ "ARS06-DBPM-4220:",
+ "ARS06-DBPM-4720:",
+ "ARS06-DBPM-5250:",
+ "ARS06-DBPM-5530:",
+ "ARS06-DBPM-5880:",
+ "ARS07-DBPM-0810:",
+ "ARS07-DBPM-0840:",
+ "ARS07-DBPM-1080:",
+ "ARS07-DBPM-1460:",
+ "ARS07-DBPM-1900:",
+ "ARS07-DBPM-2410:",
+ "ARS07-DBPM-2910:",
+ "ARS07-DBPM-4220:",
+ "ARS07-DBPM-4720:",
+ "ARS07-DBPM-5250:",
+ "ARS07-DBPM-5530:",
+ "ARS07-DBPM-5880:",
+ "ARS08-DBPM-1080:",
+ "ARS08-DBPM-1460:",
+ "ARS08-DBPM-1900:",
+ "ARS08-DBPM-2410:",
+ "ARS08-DBPM-2910:",
+ "ARS08-DBPM-4220:",
+ "ARS08-DBPM-4720:",
+ "ARS08-DBPM-5250:",
+ "ARS08-DBPM-5530:",
+ "ARS08-DBPM-5880:",
+ "ARS09-DBPM-0570:",
+ "ARS09-DBPM-1080:",
+ "ARS09-DBPM-1460:",
+ "ARS09-DBPM-1900:",
+ "ARS09-DBPM-2410:",
+ "ARS09-DBPM-2910:",
+ "ARS09-DBPM-4220:",
+ "ARS09-DBPM-4720:",
+ "ARS09-DBPM-5250:",
+ "ARS09-DBPM-5530:",
+ "ARS09-DBPM-5880:",
+ "ARS10-DBPM-1080:",
+ "ARS10-DBPM-1460:",
+ "ARS10-DBPM-1900:",
+ "ARS10-DBPM-2410:",
+ "ARS10-DBPM-2910:",
+ "ARS10-DBPM-4220:",
+ "ARS10-DBPM-4720:",
+ "ARS10-DBPM-5250:",
+ "ARS10-DBPM-5530:",
+ "ARS10-DBPM-5880:",
+ "ARS11-DBPM-0850:",
+ "ARS11-DBPM-1080:",
+ "ARS11-DBPM-1460:",
+ "ARS11-DBPM-1900:",
+ "ARS11-DBPM-2410:",
+ "ARS11-DBPM-2910:",
+ "ARS11-DBPM-4220:",
+ "ARS11-DBPM-4720:",
+ "ARS11-DBPM-5250:",
+ "ARS11-DBPM-5530:",
+ "ARS11-DBPM-5880:",
+ "ARS12-DBPM-1080:",
+ "ARS12-DBPM-1460:",
+ "ARS12-DBPM-1900:",
+ "ARS12-DBPM-2410:",
+ "ARS12-DBPM-2910:",
+ "ARS12-DBPM-4220:",
+ "ARS12-DBPM-4720:",
+ "ARS12-DBPM-5250:",
+ "ARS12-DBPM-5530:",
+ "ARS12-DBPM-5880:",
+ ]
+ for bpm in bpmnames:
+ for q in ["X", "Y", "Q"]:
+ bpmpvname = bpm + "PM-DATA-BEAM-" + q
+ PostMortemLinks.append(PV(bpmpvname))
+
+ # MBFB modes
+ # state of injection, state of pulsed magnets
+
+ ## CIRCULAR BUFFER Records from POST MORTEM IOC
+ names_circ_buffer_ioc = [
+ # ---------- BEAM CURRENT --------------
+ "CURRPCT",
+ "CURRBPM",
+ # ---------- BEAM LIFETIME --------------
+ "TAUBPM",
+ "TAUPCT",
+ # ---------- TUNES --------------
+ "QX",
+ "QY",
+ "QZ",
+ # ---------- BEAM SIZE --------------
+ "X01DD-SIGX",
+ "X01DD-SIGY",
+ "X08DB-SIGX",
+ "X08DB-SIGY",
+ # ---------- RF --------------
+ "MASTERFREQ-GET",
+ "MASTERFREQ-SET",
+ "RFPOW-0140-INP",
+ "RFPOW-0060-INP",
+ "RFPOW-0230-INP",
+ "RFPOW-0310-INP",
+ "RFPOW-0060-REF",
+ "RFPOW-0140-REF",
+ "RFPOW-0230-REF",
+ "RFPOW-0310-REF",
+ # ---------- RADFETS --------------
+ "ARS01-DDRM-0375", # Directly downstream of beam dump
+ "ABRTL-DDRM-0670", # ABRTL upstream of thick septum (also downstream of beam dump)
+ "ABRTL-DDRM-0950", # ABRTL downstream of thick septum
+ "ARS01-DDRM-1530", # End of matching section downstream of X01 (injection straight)
+ "X05LA-DDRM-0330", # Directly downstream of collimators 5
+ "ARS05-DDRM-1530", # End of matching section downstream of X05 (main RF and collimators 5)
+ "ARS05-DDRM-3500", # Center dipole of arc 5
+ "X09LA-DDRM-0330",
+ "ARS05-VMCC-0210",
+ "ARS05-VMCC-0215",
+ "ARS05-VMCC-0310",
+ "ARS05-VMCC-0315", # Directly downstream of collimators 9
+ "ARS09-DDRM-1530", # End of matching section downstream of X09 (3HC and collimators 9)
+ "ARS09-DDRM-3500", # Center dipole of arc 9
+ "ARS12-DDRM-1530", # End of matching section downstream of X12 (beginning of last arc)
+ "ARS12-DDRM-3500", # Center dipole of arc 12
+ "ARS12-DDRM-4400", # Fifth dipole of arc 12
+ "ARS12-DDRM-5480", # Start of matching section upstream of X01 (end of last arc)
+ # ---------- VACUUM --------------
+ "VACMAX",
+ "VACMAXFE",
+ "ARS01-VMCC-0110",
+ "ARS01-VMCC-0210",
+ "ARS01-VMCC-0225",
+ "ARS01-VMCC-0310",
+ "ARS01-VMCC-1550",
+ "ARS01-VMCC-3510",
+ "ARS01-VMCC-5460",
+ "ARS02-VMCC-0110",
+ "ARS02-VMCC-0210",
+ "ARS02-VMCC-0310",
+ "ARS02-VMCC-1550",
+ "ARS02-VMCC-3510",
+ "ARS02-VMCC-5460",
+ "ARS03-VMCC-0110",
+ "ARS03-VMCC-0320",
+ "ARS03-VMCC-1550",
+ "ARS03-VMCC-3510",
+ "ARS03-VMCC-5460",
+ "ARS04-VMCC-0110",
+ "ARS04-VMCC-1550",
+ "ARS04-VMCC-3510",
+ "ARS04-VMCC-5460",
+ "ARS05-VMCC-0110",
+ "ARS05-VMCC-0115",
+ "ARS05-VMCC-0210",
+ "ARS05-VMCC-0215",
+ "ARS05-VMCC-0310",
+ "ARS05-VMCC-0315",
+ "ARS05-VMCC-0410",
+ "ARS05-VMCC-0415",
+ "ARS05-VMCC-0510",
+ "ARS05-VMCC-0520",
+ "ARS05-VMCC-0530",
+ "ARS05-VMCC-0810",
+ "ARS05-VMCC-1550",
+ "ARS05-VMCC-3510",
+ "ARS05-VMCC-5460",
+ "ARS06-VMCC-0110",
+ "ARS06-VMCC-1550",
+ "ARS06-VMCC-3510",
+ "ARS06-VMCC-5460",
+ "ARS07-VMCC-0010",
+ "ARS07-VMCC-0120",
+ "ARS07-VMCC-0210",
+ "ARS07-VMCC-1550",
+ "ARS09-VMCC-0510",
+ "ARS09-VMCC-0810",
+ "ARS07-VMCC-3510",
+ "ARS07-VMCC-5460",
+ "ARS08-VMCC-0110",
+ "ARS08-VMCC-1550",
+ "ARS08-VMCC-3510",
+ "ARS08-VMCC-5460",
+ "ARS09-VMCC-0110",
+ "ARS09-VMCC-0120",
+ "ARS09-VMCC-0410",
+ "ARS09-VMCC-0510",
+ "ARS09-VMCC-0810",
+ "ARS09-VMCC-1550",
+ "ARS09-VMCC-3510",
+ "ARS09-VMCC-5460",
+ "ARS10-VMCC-0110",
+ "ARS10-VMCC-1550",
+ "ARS10-VMCC-3510",
+ "ARS10-VMCC-5460",
+ "ARS11-VMCC-0110",
+ "ARS11-VMCC-0210",
+ "ARS11-VMCC-0310",
+ "ARS11-VMCC-0410",
+ "ARS11-VMCC-0510",
+ "ARS11-VMCC-1550",
+ "ARS11-VMCC-3510",
+ "ARS11-VMCC-5460",
+ "ARS12-VMCC-0110",
+ "ARS12-VMCC-1550",
+ "ARS12-VMCC-3510",
+ "ARS12-VMCC-5460",
+ ]
+
+ for pvname in names_circ_buffer_ioc:
+ pvname = "AGEBD-POSTMORTEMLOG:BUFFER-" + pvname + "-LAST.VAL"
+ PostMortemLinks.append(PV(pvname))
+
+ # Prepare machine for accumulation
+ master_trig = PV("AGETI-CVME-MASTER-TMA:SR-Inj-Status-Sel.VAL")
+ reset_bucket = PV("AGEBD-TIMING:ZERO-TARGET-BUCKET.VAL")
+ ID_gap_ena = PV("AGEOP-STATUS:LIGHT-DELIVERY.VAL")
def __init__(self, service_name: str, pv_factory=PV):
super().__init__(service_name, pv_factory)
PV = pv_factory
- # TODO: can we do this cleaner? Maybe mechanism for
- # settings initial values in dev mode (e.g. ini file)
- if isinstance(self.onoff, LocalPVLink):
- self.onoff.value = True
+ self.CallbackPV = PV("AGESA-SBDC-0000:INT-COUNTER.VAL", auto_monitor=True)
- # Option 2:
- ## Service specific PVs from dedicated IOC
- self.my_pv2 = PV(
- "AGEBD-POSTMORTEMLOG:BO"
- ) # TODO: is this a good PV to use?
- ## Service specific PVs from other IOCs
- # ...
+class vals:
+ bdc_channels = np.array(
+ [ # see https://git.psi.ch/epics_iocboot_sls/A_MPS/-/blob/main/AGESA-CECL-BDC/AGESA-CECL-BDC_main.subs
+ "Manual EPICS Beam Dump", # 00
+ "LLRF for Storage Ring Cavity 1", # 01
+ "LLRF for Storage Ring Cavity 2", # 02
+ "LLRF for Storage Ring Cavity 3", # 03
+ "LLRF for Storage Ring Cavity 4", # 04
+ "LLRF for Super Conducting 3HC", # 05
+ "Orbit Deviation", # 06
+ "FOFB", # 07
+ "Horizontal Multi Bunch Instability", # 08
+ "Vertical Multi Bunch Instability", # 09
+ "Longitudinal Multi Bunch Instabil", # 10
+ "Filling Patern Deviation", # 11
+ "Quench of Super Conducting Super Bend", # 12
+ "Quench of High Temperature Super Conducting Undulator", # 13
+ "PSYS", # 14
+ "Vacuum", # 15
+ "Machine Interlock System", # 16
+ "Rapid Storage Ring Current Drop", # 17
+ "Beam Dump Kicker", # 18
+ "SLS A/C Power", # 19
+ "Control Room Emergency Dump", # 20
+ ]
+ )
- # TODO: class attribute names usually lowercase
- # usually it is advisable to trigger the mainloop of a service on changes of certain PVs:
- self.CallbackPV = PV(
- "AGEBD-POSTMORTEMLOG:CALLBACK", auto_monitor=dbr.DBE_VALUE
- ) # TODO: ok?
+
+def htmltable(data, header=False):
+
+ table = []
+ table.append('
') # overall table design
+
+ if header:
+ header = data[0]
+ table.append('') # header row design
+
+ for cell in header:
+ table.append('| {:} | '.format(cell)) # header cell design
+
+ table.append("
")
+ data = data[1:]
+
+ for i, row in enumerate(data):
+ if i % 2 == 0:
+ table.append('') # even row design
+ else:
+ table.append('
') # odd row design
+
+ for cell in row:
+ table.append('| {:} | '.format(cell)) # data cell design
+
+ table.append("
")
+
+ table.append("
")
+
+ return "".join(table)
+
+
+def mysleep(seconds):
+
+ t0 = perf_counter()
+ wait = True
+
+ while wait:
+ wait = perf_counter() - t0 < seconds
+
+
+def get_scopetime():
+
+ # scope address
+ scope = "LPD64-B031482.psi.ch"
+ port = 4000
+
+ # cmd for timestamp retrieval
+ cmd = "HORizontal:FASTframe:TIMEStamp:ALL?\n"
+
+ # Create a TCP socket
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(5) # Timeout in seconds
+
+ try:
+ sock.connect((scope, port))
+ sock.sendall(cmd.encode())
+
+ # Read the response
+ response = sock.recv(1024).decode().strip()
+
+ except Exception as e:
+ response = f"Error: {e:}"
+ finally:
+ sock.close()
+
+ # get time string by truncating response from index of fastfframe and quotation marks
+ date_str = response[4:-1]
+ # truncate useless nanoseconds to be compaticble with datetime
+ date_str = date_str[:-6]
+ # creat datetimeobject
+ date_obj = datetime.strptime(date_str, "%d.%m.%Y.%H:%M:%S.%f")
+ return date_obj.strftime("%Y-%m-%d %H:%M:%S.%f")
class Service(BaseService[PVs]):
@@ -56,38 +522,1218 @@ class Service(BaseService[PVs]):
):
super().__init__(name, pvs, version, sleep_interval)
- # TODO: What is this for?
- self.path = "sls/bd/bin"
+ self.logbook = elog.open(
+ "https://elog-gfa.psi.ch/SLS/",
+ user="robot",
+ password="robot",
+ use_ssl=True,
+ )
+
+ # Counters
+ self.counter_bdc_last = pvs.counter_bdc.get()
+ self.counter_dumpkicker_last = pvs.counter_dumpkicker.get()
+ self.counter_tmaster_beamdump_last = pvs.counter_tmaster_beamdump.get()
+ self.counter_tmaster_postmortem_last = pvs.counter_tmaster_postmortem.get()
+
+ self.curr = np.zeros(10)
+ self.Tshift = pvs.postdelay.get()
def update(self):
- # when callback is fired, this code will be executed
- if self.CallbackFired:
- #######################################################################################
- #######################################################################################
- # here goes all the code that your service
- # is supposed to be running when the callback is fired
- time.sleep(1)
- #######################################################################################
- #######################################################################################
+ pvs = self.pvs
+
+ curr = pvs.pctmaster.get()
+
+ if curr != self.curr[-1]:
+ self.curr[:-1] = self.curr[1:]
+ self.curr[-1] = curr
+
+ if pvs.test.get():
+ self.test = True
+ else:
+ self.test = False
+
+ # when callback fired and threshold current exceeded, write elog entry
+ if self.CallbackFired and np.nanmax(self.curr) > 3 or self.test:
+ # start stop watch
+ t0 = perf_counter()
+
+ # get timestamps
+ self.t0_hla = str(datetime.fromtimestamp(time()))
+
+ # prepare elog config and perform some machine actions depending on real or test interlock
+ if self.test:
+ self.suppress_email_notification = True
+ pvs.test.put(0)
+ print("Post mortem test request detected...")
+ else:
+ pvs.master_trig.put(0)
+ pvs.ID_gap_ena.put(1)
+ pvs.reset_bucket.put(1)
+ self.suppress_email_notification = False
+ print("Interlock beam dump detected...")
+
+ # wait for BDC latch to show interlocks
+ print(" ...waiting for BDC...")
+ while not pvs.bdcinputsum_latched.get() > 0 and perf_counter() - t0 < self.Tshift:
+ self.alive()
+ poll(evt=1.0e-5, iot=0.1)
+
+ # get BDC data
+ self.interlocks = np.array([interlock.get() for interlock in pvs.bdc_interlocks])
+ self.delays = np.array([delay.get() for delay in pvs.bdc_delays])
+
+ # get PLC alarm lists
+ self.vcs_alarmlist = pvs.vcs_alarmlist.get(as_string=True).split("\n")
+ self.mis_alarmlist = pvs.mis_alarmlist.get(as_string=True).split("\n")
+ self.misfe_alarmlist = pvs.misfe_alarmlist.get(as_string=True).split("\n")
+
+ # wait for BDC to get reset or posttrigger time tshift to pass
+ print(" ...waiting for all interlocks...")
+ while pvs.bdcinputsum_latched.get() > 0 and perf_counter() - t0 < self.Tshift:
+ self.alive()
+
+ # check for additional interlock requests to BDC
+ interlocks = np.array([interlock.get() for interlock in pvs.bdc_interlocks])
+ delays = np.array([delay.get() for delay in pvs.bdc_delays])
+ if len(np.flatnonzero(interlocks)) > len(np.flatnonzero(self.interlocks)):
+ self.interlocks = interlocks
+ self.delays = delays
+
+ # check for additional alarms in PLC alarm lists
+ vcs_alarmlist = pvs.vcs_alarmlist.get(as_string=True).split("\n")
+ if len(vcs_alarmlist) > len(self.vcs_alarmlist):
+ self.vcs_alarmlist = vcs_alarmlist
+ mis_alarmlist = pvs.mis_alarmlist.get(as_string=True).split("\n")
+ if len(mis_alarmlist) > len(self.mis_alarmlist):
+ self.mis_alarmlist = mis_alarmlist
+ misfe_alarmlist = pvs.misfe_alarmlist.get(as_string=True).split("\n")
+ if len(misfe_alarmlist) > len(self.misfe_alarmlist):
+ self.misfe_alarmlist = misfe_alarmlist
+
+ sleep(0.1)
+
+ # make sure that post trigger delay time has passed
+ while perf_counter() - t0 < self.Tshift:
+ self.alive()
+ # wait for time to pass
+ mysleep(0.01)
+ mysleep(1)
+ self.alive()
+
+ # get EPICS post mortem data including postdelay data after dump
+ self.PMD = {}
+ for pv in pvs.PostMortemLinks:
+ pvname = pv.pvname.removesuffix(".VAL")
+ pvname = pvname.removesuffix("-LAST")
+ if not pv.connected:
+ self.PMD[pvname] = np.ones(2) * np.nan
+ print(" pv not connected: {:}".format(pvname))
+ else:
+ if pvname == "AGEBD-POSTMORTEMLOG:BUFFER-FILLPATTERN-APD":
+ self.PMD[pvname] = np.reshape(pv.get(), (int(432000 / 480), 480))
+ else:
+ self.PMD[pvname] = pv.get()
+ self.PMD[pvname + ".timestamp"] = str(datetime.fromtimestamp(pv.timestamp))
+ self.t0_pmioc = self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-CURRPCT.timestamp"]
+ self.t0_pmlpd64 = get_scopetime()
+
+ # if PM IOC is running since at least 15 minutes: NSAM = 9000, SCAN = 0.1
+ Npts = len(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-CURRPCT"])
+ dt = 0.1
+ T = Npts * dt
+ self.t = np.linspace(-T + self.Tshift, -dt + self.Tshift, Npts)
+
+ Npts = len(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-FILLPATTERN-APD"])
+ dt = 1
+ T = Npts * dt
+ self.t2 = np.linspace(-T + self.Tshift, -dt + self.Tshift, Npts)
+
+ self.counter_bdc = pvs.counter_bdc.get()
+ self.t0_bdc = str(datetime.fromtimestamp(pvs.counter_bdc.timestamp))
+ self.counter_dumpkicker = pvs.counter_dumpkicker.get()
+ self.counter_tmaster_beamdump = pvs.counter_tmaster_beamdump.get()
+ self.counter_tmaster_postmortem = pvs.counter_tmaster_postmortem.get()
+
+ Npost = int(self.Tshift / 0.1)
+ self.DRMs = {
+ pvname[27:]: value
+ for pvname, value in self.PMD.items()
+ if "-DDRM-" in pvname and not ".timestamp" in pvname
+ }
+ self.DRMs_i = {
+ DRM: np.nanmin(DRMarray[-Npost - 15 : -Npost - 5])
+ for DRM, DRMarray in self.DRMs.items()
+ }
+ self.DRMs_f = {
+ DRM: np.nanmax(DRMarray[-Npost - 5 :]) for DRM, DRMarray in self.DRMs.items()
+ }
+ self.DRMs_d = {
+ DRM: np.nanmax(np.array([0, self.DRMs_f[DRM] - self.DRMs_i[DRM]]))
+ for DRM in self.DRMs
+ } # difference with negative values mapped to 0
+ self.DRMs_sum = np.nansum(np.array([self.DRMs_d[DRM] for DRM in self.DRMs]))
+ self.DRMs_p = {DRM: 100 * self.DRMs_d[DRM] / self.DRMs_sum for DRM in self.DRMs}
+
+ self.logtxt = []
+
+ # Beam Dump Controller Log
+ print("self.interlocks, ", self.interlocks)
+ print("self.delays, ", self.delays)
+ self.index1 = np.flatnonzero(self.interlocks)
+ print("self.index1", self.index1)
+ self.index2 = np.argsort(self.delays[self.index1])
+ print("self.index2", self.index2)
+
+ if self.test:
+ self.firstinterlock = "Test"
+ elif len(self.index1) == 0:
+ self.firstinterlock = "BDC Readout Issue"
+ else:
+ self.firstinterlock = vals.bdc_channels[self.index1][
+ self.index2[0]
+ ] # This lines often crashes
+
+ print(" ...interlock triggered by {:}...".format(self.firstinterlock))
+
+ self.bdc_delays = [
+ "{:.2f} µs".format(self.delays[self.index1][i]) for i in self.index2
+ ]
+ self.bdc_channel_description = [
+ "{:}".format(vals.bdc_channels[self.index1][i]) for i in self.index2
+ ]
+ self.bdc_channel_nr = ["{:02}".format(self.index1[i]) for i in self.index2]
+
+ self.logtxt.append("Beam Dump Controller
")
+ self.logtxt.append(
+ htmltable(
+ [["Delay", "Description", "Channel Nr."]]
+ + [
+ [
+ "{:.2f} µs".format(self.delays[self.index1][i]),
+ "{:}".format(vals.bdc_channels[self.index1][i]),
+ "{:02}".format(self.index1[i]),
+ ]
+ for i in self.index2
+ ],
+ header=True,
+ )
+ )
+
+ self.alive()
+
+ # VCS Alarm List
+ self.logtxt.append("VCS
")
+ self.logtxt.append(self.vcs_alarmlist[0])
+ (
+ self.vcs_alarmlist_alarmindex,
+ self.vcs_alarmlist_timestamps,
+ self.vcs_alarmlist_descriptions,
+ ) = self.GascheLines2HTMLtable(self.vcs_alarmlist[2:])
+
+ self.alive()
+
+ # MIS Alarm List
+ self.logtxt.append("MIS
")
+ self.logtxt.append(self.mis_alarmlist[0])
+ (
+ self.mis_alarmlist_alarmindex,
+ self.mis_alarmlist_timestamps,
+ self.mis_alarmlist_descriptions,
+ ) = self.GascheLines2HTMLtable(self.mis_alarmlist[2:])
+
+ self.alive()
+
+ # MIS-FE Alarm List
+ self.logtxt.append("MIS-FE
")
+ self.logtxt.append(self.misfe_alarmlist[0])
+ (
+ self.misfe_alarmlist_alarmindex,
+ self.misfe_alarmlist_timestamps,
+ self.misfe_alarmlist_descriptions,
+ ) = self.GascheLines2HTMLtable(self.misfe_alarmlist[2:])
+
+ self.alive()
+
+ # RadFETs
+ drmdat = lambda DRM: [
+ "{:.4f}".format(self.DRMs_i[DRM]),
+ "{:.4f}".format(self.DRMs_f[DRM]),
+ "{:.4f}".format(self.DRMs_d[DRM]),
+ "{:.2f}".format(self.DRMs_p[DRM]),
+ ]
+ self.logtxt.append("RadFETs
")
+ self.logtxt.append(
+ htmltable(
+ [
+ [
+ "PV",
+ "Description",
+ "Before Dump / Gy",
+ "After Dump / Gy",
+ "Delta / Gy",
+ "Distribution / %",
+ ],
+ ["ARS01-DDRM-0375", "Directly downstream of beam dump"]
+ + drmdat("ARS01-DDRM-0375"),
+ [
+ "ABRTL-DDRM-0670",
+ "ABRTL upstream of thick septum (also downstream of beam dump)",
+ ]
+ + drmdat("ABRTL-DDRM-0670"),
+ ["ABRTL-DDRM-0950", "ABRTL downstream of thick septum"]
+ + drmdat("ABRTL-DDRM-0950"),
+ [
+ "ARS01-DDRM-1530",
+ "End of matching section downstream of X01 (injection straight)",
+ ]
+ + drmdat("ARS01-DDRM-1530"),
+ ["X05LA-DDRM-0330", "Directly downstream of collimators 5"]
+ + drmdat("X05LA-DDRM-0330"),
+ [
+ "ARS05-DDRM-1530",
+ "End of matching section downstream of X05 (main RF and collimators 5)",
+ ]
+ + drmdat("ARS05-DDRM-1530"),
+ ["ARS05-DDRM-3500", "Center dipole of arc 5"] + drmdat("ARS05-DDRM-3500"),
+ ["X09LA-DDRM-0330", "Directly downstream of collimators 9"]
+ + drmdat("X09LA-DDRM-0330"),
+ [
+ "ARS09-DDRM-1530",
+ "End of matching section downstream of X09 (3HC and collimators 9)",
+ ]
+ + drmdat("ARS09-DDRM-1530"),
+ ["ARS09-DDRM-3500", "Center dipole of arc 9 "] + drmdat("ARS09-DDRM-3500"),
+ [
+ "ARS12-DDRM-1530",
+ "End of matching section downstream of X12 (beginning of last arc)",
+ ]
+ + drmdat("ARS12-DDRM-1530"),
+ ["ARS12-DDRM-3500", "Center dipole of arc 12"] + drmdat("ARS12-DDRM-3500"),
+ ["ARS12-DDRM-4400", "Fifth dipole of arc 12"] + drmdat("ARS12-DDRM-4400"),
+ [
+ "ARS12-DDRM-5480",
+ "Start of matching section upstream of X01 (end of last arc)",
+ ]
+ + drmdat("ARS12-DDRM-5480"),
+ ],
+ header=True,
+ )
+ )
+
+ self.alive()
+
+ # Timestamps
+ self.logtxt.append("Timestamps
")
+ self.logtxt.append(
+ htmltable(
+ [
+ ["BDC Timestamp", "{:}".format(str(self.t0_bdc)[:-3])],
+ ["HLA Timestamp", "{:}".format(str(self.t0_hla)[:-3])],
+ ["PM IOC Timestamp", "{:}".format(str(self.t0_pmioc)[:-3])],
+ ["PM Scope LPD64", "{:}".format(str(self.t0_pmlpd64)[:-3])],
+ ],
+ header=False,
+ )
+ )
+
+ # Counters
+ self.logtxt.append("Counters
")
+ self.logtxt.append(
+ htmltable(
+ [
+ ["Counter", "Value", "Last Value", "Delta"],
+ [
+ "BDC Dump Counter",
+ "{:.0f}".format(self.counter_bdc),
+ "{:.0f}".format(self.counter_bdc_last),
+ "{:.0f}".format(self.counter_bdc - self.counter_bdc_last),
+ ],
+ [
+ "Beam Dump Kicker Counter",
+ "{:.0f}".format(self.counter_dumpkicker),
+ "{:.0f}".format(self.counter_dumpkicker_last),
+ "{:.0f}".format(self.counter_dumpkicker - self.counter_dumpkicker_last),
+ ],
+ [
+ "Beam Dump Event Counter",
+ "{:.0f}".format(self.counter_tmaster_beamdump),
+ "{:.0f}".format(self.counter_tmaster_beamdump_last),
+ "{:.0f}".format(
+ self.counter_tmaster_beamdump - self.counter_tmaster_beamdump_last
+ ),
+ ],
+ [
+ "Post Mortem Event Counter",
+ "{:.0f}".format(self.counter_tmaster_postmortem),
+ "{:.0f}".format(self.counter_tmaster_postmortem_last),
+ "{:.0f}".format(
+ self.counter_tmaster_postmortem
+ - self.counter_tmaster_postmortem_last
+ ),
+ ],
+ ],
+ header=True,
+ )
+ )
+
+ self.counter_bdc_last = self.counter_bdc
+ self.counter_dumpkicker_last = self.counter_dumpkicker
+ self.counter_tmaster_beamdump_last = self.counter_tmaster_beamdump
+ self.counter_tmaster_postmortem_last = self.counter_tmaster_postmortem
+
+ self.alive()
+
+ # assign dump to system
+ if self.firstinterlock in [
+ "LLRF for Storage Ring Cavity 1",
+ "LLRF for Storage Ring Cavity 2",
+ "LLRF for Storage Ring Cavity 3",
+ "LLRF for Storage Ring Cavity 4",
+ "LLRF for Super Conducting 3HC",
+ ]:
+ system = "HF"
+ elif self.firstinterlock in ["Orbit Deviation"]:
+ system = "BPMs"
+ elif self.firstinterlock in [
+ "FOFB",
+ "Horizontal Multi Bunch Instability",
+ "Vertical Multi Bunch Instability",
+ "Longitudinal Multi Bunch Instabil",
+ "Filling Patern Deviation",
+ ]:
+ system = "Feedbacks"
+ elif self.firstinterlock in ["Vacuum"]:
+ system = "Vakuum"
+ elif self.firstinterlock in ["PSYS"]:
+ system = "PSYS"
+ elif self.firstinterlock in ["Machine Interlock System"]:
+ system = "MachineInterlockSystem"
+ elif self.firstinterlock in ["Test"]:
+ system = "Test"
+ else:
+ system = "Unbekannt"
+
+ # prepare plots for attachment
+ fig1 = self.makeplots()
+
+ self.alive()
+
+ try:
+ fig2 = self.lpd64plot()
+ file2 = "/sls/op/data/applications/postmortem/{:}_BeamDumpLPD64.png".format(
+ EpicsTimestampStr2FileTimestampStr(self.t0_hla)
+ )
+ fig2.savefig(file2, dpi=192, bbox_inches="tight")
+ file2 = [file2]
+ plt.close(fig2)
+ except:
+ file2 = []
+ print("LPD64 plot failed")
+
+ self.alive()
+
+ file1 = "/sls/op/data/applications/postmortem/{:}_PostMortem.png".format(
+ EpicsTimestampStr2FileTimestampStr(self.t0_hla)
+ )
+ fig1.savefig(file1, dpi=192, bbox_inches="tight")
+ self.list_of_attachments = file2 + [file1]
+ plt.close(fig1)
+
+ self.alive()
+
+ # prepare data file
+ if self.test:
+ file3 = "/sls/op/data/applications/postmortem/{:}_PostMortem_Test.h5".format(
+ EpicsTimestampStr2FileTimestampStr(self.t0_hla)
+ )
+ else:
+ file3 = "/sls/op/data/applications/postmortem/{:}_PostMortem.h5".format(
+ EpicsTimestampStr2FileTimestampStr(self.t0_hla)
+ )
+ data = {
+ "t0_hla": self.t0_hla,
+ "t0_pmioc": self.t0_pmioc,
+ "t0_bdc": self.t0_bdc,
+ "t0_lpd64": self.t0_pmlpd64,
+ "t": self.t,
+ "t2": self.t2,
+ "bdc_delays": self.bdc_delays,
+ "bdc_channel_description": self.bdc_channel_description,
+ "bdc_channel_nr": self.bdc_channel_nr,
+ "counter_bdc": self.counter_bdc,
+ "counter_dumpkicker": self.counter_dumpkicker,
+ "counter_tmaster_beamdump": self.counter_tmaster_beamdump,
+ "counter_tmaster_postmortem": self.counter_tmaster_postmortem,
+ "vcs_alarmlist_infostring": self.vcs_alarmlist[0],
+ "vcs_alarmlist_alarmindex": self.vcs_alarmlist_alarmindex,
+ "vcs_alarmlist_timestamps": self.vcs_alarmlist_timestamps,
+ "vcs_alarmlist_descriptions": self.vcs_alarmlist_descriptions,
+ "mis_alarmlist_infostring": self.mis_alarmlist[0],
+ "mis_alarmlist_alarmindex": self.mis_alarmlist_alarmindex,
+ "mis_alarmlist_timestamps": self.mis_alarmlist_timestamps,
+ "mis_alarmlist_descriptions": self.mis_alarmlist_descriptions,
+ "misfe_alarmlist_infostring": self.misfe_alarmlist[0],
+ "misfe_alarmlist_alarmindex": self.misfe_alarmlist_alarmindex,
+ "misfe_alarmlist_timestamps": self.misfe_alarmlist_timestamps,
+ "misfe_alarmlist_descriptions": self.misfe_alarmlist_descriptions,
+ }
+ data.update(self.PMD)
+
+ self.alive()
+
+ # Write to elog
+ self.logtxt = "".join(self.logtxt)
+
+ self.dict_of_attributes = {
+ "Wann": time(),
+ "Autor": "Post Mortem Logger",
+ "Eintrag": "Post Mortem",
+ "Effekt": "Downtime",
+ "System": "{:}".format(system),
+ "Titel": "Interlock Beam Dump {:.0f}, {:.0f} mA".format(
+ self.counter_bdc, np.nanmax(self.curr)
+ ),
+ "Encoding": "html",
+ }
+
+ self.alive()
+
+ print(" ...elog entry prepared...")
+
+ self.msg_id = self.logbook.post(
+ self.logtxt,
+ attributes=self.dict_of_attributes,
+ attachments=self.list_of_attachments,
+ suppress_email_notification=self.suppress_email_notification,
+ attribute_as_param="value",
+ )
+ print(" ...elog entry submitted...")
+
+ self.alive()
+
+ h5save(file3, data, timestamp=False)
+ print(" {:}".format(file3))
+ print(" ...post mortem data saved")
- # callback done
self.CallbackFired = 0
- #######################################################################################
- #######################################################################################
- # here goes all the code that your service
- # is supposed to be running in any case, e.g.,
+ elif self.CallbackFired:
+ print("Beam dump below threshold current detected.")
+ # upate counters for beamdumps from below threshold beam current
+ self.counter_bdc_last = pvs.counter_bdc.get()
+ self.counter_dumpkicker_last = pvs.counter_dumpkicker.get()
+ self.counter_tmaster_beamdump_last = pvs.counter_tmaster_beamdump.get()
+ self.counter_tmaster_postmortem_last = pvs.counter_tmaster_postmortem.get()
+ self.CallbackFired = 0
- # update some pvs/vals
- my_pv1_random_val = str(time.clock_gettime_ns(0))[-1]
- self.pvs.my_pv1.put(my_pv1_random_val)
- # self.pvs.my_pv2.put("MY_PV2 value")
+ def alive(self):
+ # inform watchdog(s) for service run/main state
+ self.pvs.running.put(0)
+ self.pvs.alive.put(0)
- # get some pvs/vals
- self.val1 = self.pvs.my_pv1.get()
- self.val2 = self.pvs.my_pv2.get()
+ def GascheLines2HTMLtable(self, lines):
- # maybe sleep to limit the update loop execution rate
- time.sleep(1)
- #######################################################################################
- #######################################################################################
+ timestamps = [
+ (
+ datetime.strptime(line[6:30], "%Y-%m-%d %H:%M:%S.%f") - datetime(1970, 1, 1)
+ ).total_seconds()
+ for line in lines
+ ]
+ sortindex = np.argsort(timestamps)
+
+ alarmindex = [lines[i][1:4] for i in sortindex]
+ timestamps = [lines[i][6:30] for i in sortindex]
+ descriptions = [lines[i][33:] for i in sortindex]
+
+ self.logtxt.append(
+ htmltable(
+ [["Alarm", "Timestamp", "Description"]]
+ + [[lines[i][1:4], lines[i][6:30], lines[i][33:]] for i in sortindex],
+ header=True,
+ )
+ )
+
+ return alarmindex, timestamps, descriptions
+
+ def makeplots(self):
+
+ # prepare figure
+ Nplotrows = 14
+ Nplotcols = 1
+ bbox = (1.1, 1)
+ fig = plt.figure(figsize=(20, 6 * (Nplotrows + 4)), constrained_layout=True)
+ height_ratios = np.ones(Nplotrows)
+ height_ratios[4] = 3
+ height_ratios[5] = 3
+ GS = fig.add_gridspec(Nplotrows, Nplotcols, height_ratios=list(height_ratios))
+
+ self.alive()
+ # Beam Current and Lifetime
+ index = 0
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-CURRBPM"],
+ "-xg",
+ label="AGEBD-DBPM3CURR:CURRENT-AVG",
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-CURRPCT"],
+ "-x",
+ color="lawngreen",
+ label="ARS07-DPCT-0000:CURR",
+ )
+ ax.tick_params(axis="y", colors="g", which="both")
+ ax.set_ylabel("Beam current / mA", color="g")
+ ax.set_xlim([self.t[0], self.t[-1]])
+
+ ax2 = ax.twinx()
+ ax2.plot(self.t, self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-TAUBPM"], "-xb", label="TAUBPM")
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-TAUPCT"],
+ "-x",
+ color="cornflowerblue",
+ label="TAUPCT",
+ )
+ ax2.tick_params(axis="y", colors="b", which="both")
+ lines, labels = ax.get_legend_handles_labels()
+ lines2, labels2 = ax2.get_legend_handles_labels()
+ lgnd = ax2.legend(lines + lines2, labels + labels2, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax2.set_ylabel("Beam lifetime / hours", color="b")
+ ax2.set_xlim([self.t[0], self.t[-1]])
+ ax2.set_ylim([0, 25])
+
+ self.alive()
+ # Tunes
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-QX"],
+ "-x",
+ color="#2A63E4",
+ label="AGEBD-TUNEBUMP:QX",
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-QY"],
+ "-x",
+ color="#8B1A96",
+ label="AGEBD-TUNEBUMP:QY",
+ )
+ ax.set_ylabel("Transverse Tunes")
+ ax.set_xlim([self.t[0], self.t[-1]])
+ ax2 = ax.twinx()
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-QZ"],
+ "-x",
+ color="#cd6100",
+ label="GEBD-TUNEBUMP:QZ",
+ )
+ ax2.set_ylabel("Synchrotron Tune")
+ lines, labels = ax.get_legend_handles_labels()
+ lines2, labels2 = ax2.get_legend_handles_labels()
+ lgnd = ax2.legend(lines + lines2, labels + labels2, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+
+ self.alive()
+ # Beam Size X01DD
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X01DD-SIGY"],
+ "-x",
+ color="#8B1A96",
+ label="$\langle\sigma_y\\rangle$={:.1f} µm".format(
+ np.nanmean(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X01DD-SIGY"])
+ ),
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X01DD-SIGX"],
+ "-x",
+ color="#2A63E4",
+ label="$\langle\sigma_x\\rangle$={:.1f} µm".format(
+ np.nanmean(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X01DD-SIGX"])
+ ),
+ )
+ lines, labels = ax.get_legend_handles_labels()
+ lgnd = ax.legend(lines, labels, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("FZP beam size X01DD / µm")
+ ax.set_xlim([self.t[0], self.t[-1]])
+ ax.set_ylim([0, 30])
+
+ self.alive()
+ # Beam Size X08DB
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X08DB-SIGY"],
+ "-x",
+ color="#8B1A96",
+ label="$\langle\sigma_y\\rangle$={:.1f} µm".format(
+ np.nanmean(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X08DB-SIGY"])
+ ),
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X08DB-SIGX"],
+ "-x",
+ color="#2A63E4",
+ label="$\langle\sigma_x\\rangle$={:.1f} µm".format(
+ np.nanmean(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-X08DB-SIGX"])
+ ),
+ )
+ lines, labels = ax.get_legend_handles_labels()
+ lgnd = ax.legend(lines, labels, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("FZP beam size X08DB / µm")
+ ax.set_xlim([self.t[0], self.t[-1]])
+ ax.set_ylim([0, 30])
+
+ self.alive()
+ # Highest Vacuum Pressure for Machine and Frontend
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-VACMAX"],
+ "-xy",
+ label="ARIVA-VMAVE:PRESS-MAX",
+ )
+ ax.tick_params(axis="y", colors="y", which="both")
+ ax.set_ylabel("Vacuum pressure / mbar", color="y")
+ ax.set_xlim([self.t[0], self.t[-1]])
+
+ ax2 = ax.twinx()
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-VACMAXFE"],
+ "-x",
+ color="orange",
+ label="ARIVA-VMAVE-FE:PRESS-MAX",
+ )
+ ax2.tick_params(axis="y", colors="orange", which="both")
+ lines, labels = ax.get_legend_handles_labels()
+ lines2, labels2 = ax2.get_legend_handles_labels()
+ lgnd = ax2.legend(lines + lines2, labels + labels2, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax2.set_ylabel("Vacuum pressure / mbar", color="orange")
+ ax2.set_xlim([self.t[0], self.t[-1]])
+
+ self.alive()
+ # Absolute Machine Vacuum Peaks
+ index += 1
+ ax = fig.add_subplot(GS[index])
+
+ names_macvac = [key for key in self.PMD if "-VMCC-" in key and not ".timestamp" in key]
+ ranking = np.zeros(len(names_macvac))
+
+ for i, pvname in enumerate(names_macvac):
+ ranking[i] = np.nanmax(self.PMD[pvname])
+
+ for i in np.argsort(ranking)[::-1]:
+ pvname = names_macvac[i]
+ ax.semilogy(
+ self.t,
+ self.PMD[pvname],
+ label="{:}, $p_{{max}}=$ {:.1e}".format(pvname[27:], ranking[i]),
+ )
+
+ ax.set_ylabel("Sorted machine vacuum pressure")
+ ax.set_xlim([self.t[0], self.t[-1]])
+ ax.legend(loc="upper left", bbox_to_anchor=bbox, ncol=2)
+
+ self.alive()
+ # Relative Machine Vacuum Peaks
+ index += 1
+ ax = fig.add_subplot(GS[index])
+
+ datarel = {}
+ ranking = np.zeros(len(names_macvac))
+
+ for i, pvname in enumerate(names_macvac):
+ datarel[pvname] = self.PMD[pvname] / np.nanmin(self.PMD[pvname])
+ ranking[i] = np.nanmax(datarel[pvname])
+
+ for i in np.argsort(ranking)[::-1]:
+ pvname = names_macvac[i]
+ ax.plot(
+ self.t,
+ datarel[pvname],
+ label="{:}, $\delta_{{max}}=$ {:.1f}".format(pvname[27:], ranking[i]),
+ )
+
+ ax.set_ylabel("Sorted relative vacuum pressure change, $\delta_{{max}}=max(p/p_{{min}})$ ")
+ ax.set_ylim([1, np.nanmax(ranking)])
+ ax.set_xlim([self.t[0], self.t[-1]])
+ ax.legend(loc="upper left", bbox_to_anchor=bbox, ncol=2)
+
+ self.alive()
+ # RF Master Oscillator Frequency
+ rf_ref = 499652000
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ rf_ref - self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-MASTERFREQ-SET"],
+ "-xr",
+ label="AGERF-MO01:FREQ-SET",
+ )
+ ax.plot(
+ self.t,
+ rf_ref - self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-MASTERFREQ-GET"],
+ "-xm",
+ label="AGERF-MO01:FREQ-GET",
+ )
+ ax.tick_params(axis="y", colors="m", which="both")
+ ax.get_yaxis().get_major_formatter().set_scientific(False)
+ ax.get_yaxis().get_major_formatter().set_useOffset(False)
+ lgnd = ax.legend(loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel(
+ "Master oscillator frequency offset to {:.0f} kHz / Hz".format(rf_ref / 1000), color="m"
+ )
+ ax.set_xlim([self.t[0], self.t[-1]])
+
+ self.alive()
+ # RF Power In/Reflected
+ index += 1
+ c = "m"
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0060-INP"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0060:CO1i-POWER-MAX",
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0140-INP"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0140:CO1i-POWER-MAX",
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0230-INP"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0230:CO1i-POWER-MAX",
+ )
+ ax.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0310-INP"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0310:CO1i-POWER-MAX",
+ )
+ ax.tick_params(axis="y", colors=c, which="both")
+ ax.set_ylabel("Total main RF forward power / kW", color=c)
+ ax.set_xlabel("Time since beam dump / s")
+ ax.set_xlim([self.t[0], self.t[-1]])
+
+ ax2, c = ax.twinx(), "r"
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0060-REF"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0060:CO1r-POWER-MAX",
+ )
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0140-REF"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0140:CO1r-POWER-MAX",
+ )
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0230-REF"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0230:CO1r-POWER-MAX",
+ )
+ ax2.plot(
+ self.t,
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-RFPOW-0310-REF"],
+ "-x",
+ color=c,
+ label="$\\Sigma$ ARS05-RCAV-0310:CO1r-POWER-MAX",
+ )
+ ax2.tick_params(axis="y", colors=c, which="both")
+ lines, labels = ax.get_legend_handles_labels()
+ lines2, labels2 = ax2.get_legend_handles_labels()
+ lgnd = ax2.legend(lines + lines2, labels + labels2, loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax2.set_ylabel("Total main RF reflected power / kW", color=c)
+ ax2.set_xlim([self.t[0], self.t[-1]])
+
+ self.alive()
+ # 4 Kicker Bump
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.PMD["ARS01-MKIK-0140:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0140:CURRENT-WF"],
+ "-x",
+ label="Kicker 1, " + self.PMD["ARS01-MKIK-0140:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0220:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0220:CURRENT-WF"],
+ "-x",
+ label="Kicker 2, " + self.PMD["ARS01-MKIK-0220:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0850:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0850:CURRENT-WF"],
+ "-x",
+ label="Kicker 3, " + self.PMD["ARS01-MKIK-0850:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0940:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0940:CURRENT-WF"],
+ "-x",
+ label="Kicker 4, " + self.PMD["ARS01-MKIK-0940:CURRENT-WF.timestamp"][:-5],
+ )
+ lgnd = ax.legend(loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("4-kicker bump kicker current / A")
+ ax.set_xlabel("Time since trigger / us")
+ ax.set_xlim(
+ [self.PMD["ARS01-MKIK-0140:TIME-AXIS"][0], self.PMD["ARS01-MKIK-0140:TIME-AXIS"][-1]]
+ )
+
+ self.alive()
+ # Beam Dump Kicker
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.PMD["ARS01-MKIK-0050-1:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0050-1:CURRENT-WF"],
+ "-x",
+ label="Stage 1, " + self.PMD["ARS01-MKIK-0050-1:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0050-2:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0050-2:CURRENT-WF"],
+ "-x",
+ label="Stage 2, " + self.PMD["ARS01-MKIK-0050-2:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0050-3:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0050-3:CURRENT-WF"],
+ "-x",
+ label="Stage 3, " + self.PMD["ARS01-MKIK-0050-3:CURRENT-WF.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-MKIK-0050-4:TIME-AXIS"],
+ self.PMD["ARS01-MKIK-0050-4:CURRENT-WF"],
+ "-x",
+ label="Stage 4, " + self.PMD["ARS01-MKIK-0050-4:CURRENT-WF.timestamp"][:-5],
+ )
+ lgnd = ax.legend(loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("Beam dump kicker current / A")
+ ax.set_xlabel("Time since trigger / us")
+ ax.set_xlim(
+ [
+ self.PMD["ARS01-MKIK-0050-1:TIME-AXIS"][0],
+ self.PMD["ARS01-MKIK-0050-1:TIME-AXIS"][-1],
+ ]
+ )
+
+ self.alive()
+ # Libera BLMs 1
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.PMD["ABODI-DFBL:SigPm.A"],
+ "-x",
+ label="ABODI-DFBL:SigPm.A, " + self.PMD["ABODI-DFBL:SigPm.A.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ABODI-DFBL:SigPm.B"],
+ "-x",
+ label="ABODI-DFBL:SigPm.B, " + self.PMD["ABODI-DFBL:SigPm.B.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ABODI-DFBL:SigPm.C"],
+ "-x",
+ label="ABODI-DFBL:SigPm.C, " + self.PMD["ABODI-DFBL:SigPm.C.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ABODI-DFBL:SigPm.D"],
+ "-x",
+ label="ABODI-DFBL:SigPm.D, " + self.PMD["ABODI-DFBL:SigPm.D.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-DFBL:SigPm.A"],
+ "-x",
+ label="ARS01-DFBL:SigPm.A, " + self.PMD["ARS01-DFBL:SigPm.A.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-DFBL:SigPm.B"],
+ "-x",
+ label="ARS01-DFBL:SigPm.B, " + self.PMD["ARS01-DFBL:SigPm.B.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-DFBL:SigPm.C"],
+ "-x",
+ label="ARS01-DFBL:SigPm.C, " + self.PMD["ARS01-DFBL:SigPm.C.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS01-DFBL:SigPm.D"],
+ "-x",
+ label="ARS01-DFBL:SigPm.D, " + self.PMD["ARS01-DFBL:SigPm.D.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS02-DFBL:SigPm.A"],
+ "-x",
+ label="ARS02-DFBL:SigPm.A, " + self.PMD["ARS02-DFBL:SigPm.A.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS02-DFBL:SigPm.B"],
+ "-x",
+ label="ARS02-DFBL:SigPm.B, " + self.PMD["ARS02-DFBL:SigPm.B.timestamp"][:-5],
+ )
+ lgnd = ax.legend(loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("Libera BLM SUM signal Post Mortem Buffer / (a.u.)")
+ ax.set_xlabel("Turns")
+
+ self.alive()
+ # Libera BLMs 2
+ index += 1
+ ax = fig.add_subplot(GS[index])
+ ax.plot(
+ self.PMD["ARS02-DFBL:SigPm.C"],
+ "-x",
+ label="ARS02-DFBL:SigPm.C, " + self.PMD["ARS02-DFBL:SigPm.C.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS02-DFBL:SigPm.D"],
+ "-x",
+ label="ARS02-DFBL:SigPm.D, " + self.PMD["ARS02-DFBL:SigPm.D.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS06-DFBL:SigPm.A"],
+ "-x",
+ label="ARS06-DFBL:SigPm.A, " + self.PMD["ARS06-DFBL:SigPm.A.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS06-DFBL:SigPm.B"],
+ "-x",
+ label="ARS06-DFBL:SigPm.B, " + self.PMD["ARS06-DFBL:SigPm.B.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS06-DFBL:SigPm.C"],
+ "-x",
+ label="ARS06-DFBL:SigPm.C, " + self.PMD["ARS06-DFBL:SigPm.C.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS06-DFBL:SigPm.D"],
+ "-x",
+ label="ARS06-DFBL:SigPm.D, " + self.PMD["ARS06-DFBL:SigPm.D.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS09-DFBL:SigPm.A"],
+ "-x",
+ label="ARS09-DFBL:SigPm.A, " + self.PMD["ARS09-DFBL:SigPm.A.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS09-DFBL:SigPm.B"],
+ "-x",
+ label="ARS09-DFBL:SigPm.B, " + self.PMD["ARS09-DFBL:SigPm.B.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS09-DFBL:SigPm.C"],
+ "-x",
+ label="ARS09-DFBL:SigPm.C, " + self.PMD["ARS09-DFBL:SigPm.C.timestamp"][:-5],
+ )
+ ax.plot(
+ self.PMD["ARS09-DFBL:SigPm.D"],
+ "-x",
+ label="ARS09-DFBL:SigPm.D, " + self.PMD["ARS09-DFBL:SigPm.D.timestamp"][:-5],
+ )
+ lgnd = ax.legend(loc="upper left", bbox_to_anchor=bbox)
+ for handle in lgnd.legendHandles:
+ handle._markersize = 7
+ handle._linewidth = 2
+ ax.set_ylabel("Libera BLM SUM signal Post Mortem Buffer / (a.u.)")
+ ax.set_xlabel("Turns")
+
+ self.alive()
+ # Filling Pattern
+ index += 1
+ ax = fig.add_subplot(GS[index], projection="3d")
+ # X-AXIS is for the bucket nr.
+ X_bucket = np.arange(480) + 1
+ # Y-AXIS is for the filled charge (the first dataset is the newest)
+ Ndat = len(self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-FILLPATTERN-APD"])
+ # Z-AXIS is for the individual plots, i.e., the time before the dump
+ Z_Nplots = self.t2
+ # Color-AXIS is duplicate of the Z and time axis for easier visual
+ N = 5
+ colors = np.tile(plt.cm.get_cmap("hsv", Ndat / N)(np.linspace(0, 1, int(Ndat / N))), (N, 1))
+
+ def plot2din3d(x, y, z, c):
+ ax.plot(x, y, zs=z, zdir="z", color=c)
+ col_obj = ax.fill_between(x, 0, y, step="pre", color=c, alpha=0.1)
+ ax.add_collection3d(col_obj, zs=z, zdir="z")
+
+ for y, z, c in zip(
+ self.PMD["AGEBD-POSTMORTEMLOG:BUFFER-FILLPATTERN-APD"], Z_Nplots, colors
+ ):
+ plot2din3d(X_bucket, y, z, c)
+
+ ax.set_xlabel("Bucket nr.")
+ ax.set_ylabel("Charge / nC")
+ ax.set_zlabel("Time since beam dump / s")
+ ax.view_init(elev=40.0, azim=80)
+ ax.set_box_aspect((16, 9, 5), zoom=1.2)
+
+ return fig
+
+ def lpd64plot(self):
+
+ fig, (axu, axl) = plt.subplots(
+ 2,
+ 1,
+ figsize=(20, 9),
+ sharex=True,
+ tight_layout=True,
+ gridspec_kw={"height_ratios": [1, 3], "hspace": 0},
+ )
+
+ (l4,) = axu.plot(
+ self.PMD["ARF-LPD64:X-AXIS-CH1"] / 1e3,
+ self.PMD["ARF-LPD64:CH1-DATA"],
+ "y+-",
+ label="Filling Pattern BPM ARS01-DBPM-0530",
+ lw=1,
+ )
+ axu.set_ylabel("BPM Sum / V")
+ axu.set_ylim([-80, 80])
+ axu.set_xlim(
+ [
+ np.nanmin(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ np.nanmax(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ ]
+ )
+
+ (l1,) = axl.plot(
+ self.PMD["ARF-LPD64:X-AXIS-CH4"] / 1e3,
+ self.PMD["ARF-LPD64:CH4-DATA"] / 1e3,
+ "g+-",
+ label="Beam Dump Kicker Magnet, BDC delay = {:.0f} ns".format(
+ self.PMD["AGESA-SBDC-0000:BUNCH0_MAGNET_RAMP_DELAY"]
+ ),
+ lw=1,
+ )
+ axl.set_ylabel("Beam Dump Kicker Pulse / V")
+ axl.set_xlabel("Time relative to dump kick / µs")
+ axl.set_ylim([-0.09, 1.85])
+ axl.set_xlim(
+ [
+ np.nanmin(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ np.nanmax(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ ]
+ )
+ axl.xaxis.set_ticks(np.arange(-1400 / 1e3, 1401 / 1e3, 200 / 1e3))
+ axl.xaxis.grid()
+
+ axt = axl.twinx()
+
+ i1 = np.where(
+ np.abs(self.PMD["ARF-LPD64:CH3-DATA"]) > np.nanmax(self.PMD["ARF-LPD64:CH3-DATA"]) / 4
+ )[0][0]
+ i2 = np.where(
+ np.abs(self.PMD["ARF-LPD64:CH3-DATA"]) > np.nanmax(self.PMD["ARF-LPD64:CH3-DATA"]) / 4
+ )[0][-1]
+ dt1 = self.PMD["ARF-LPD64:X-AXIS-CH3"][i2] - self.PMD["ARF-LPD64:X-AXIS-CH3"][i1]
+ axt.axvspan(
+ self.PMD["ARF-LPD64:X-AXIS-CH3"][i1] / 1e3,
+ self.PMD["ARF-LPD64:X-AXIS-CH3"][i2] / 1e3,
+ color="red",
+ alpha=0.1,
+ )
+ (l2,) = axt.plot(
+ self.PMD["ARF-LPD64:X-AXIS-CH3"] / 1e3,
+ np.abs(self.PMD["ARF-LPD64:CH3-DATA"]),
+ "r-",
+ label="Beam Dump Cherenkov Fiber, Δt={:.1f} ns".format(dt1),
+ lw=1,
+ )
+
+ i1 = np.where(
+ np.abs(self.PMD["ARF-LPD64:CH2-DATA"]) > np.nanmax(self.PMD["ARF-LPD64:CH2-DATA"]) / 4
+ )[0][0]
+ i2 = np.where(
+ np.abs(self.PMD["ARF-LPD64:CH2-DATA"]) > np.nanmax(self.PMD["ARF-LPD64:CH2-DATA"]) / 4
+ )[0][-1]
+ dt2 = self.PMD["ARF-LPD64:X-AXIS-CH2"][i2] - self.PMD["ARF-LPD64:X-AXIS-CH2"][i1]
+ axt.axvspan(
+ self.PMD["ARF-LPD64:X-AXIS-CH2"][i1] / 1e3,
+ self.PMD["ARF-LPD64:X-AXIS-CH2"][i2] / 1e3,
+ color="blue",
+ alpha=0.1,
+ )
+ (l3,) = axt.plot(
+ self.PMD["ARF-LPD64:X-AXIS-CH2"] / 1e3,
+ np.abs(self.PMD["ARF-LPD64:CH2-DATA"]),
+ "b-",
+ label="Septa Cherenkov Fiber, Δt={:.1f} ns".format(dt2),
+ lw=1,
+ )
+ axt.set_ylabel("Photomultiplier / mV")
+ axt.set_ylim([0, 1500])
+ axt.set_xlim(
+ [
+ np.nanmin(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ np.nanmax(self.PMD["ARF-LPD64:X-AXIS-CH4"]) / 1e3,
+ ]
+ )
+ axt.grid()
+
+ leg = axt.legend(
+ handles=[l4, l1, l2, l3],
+ loc="upper left",
+ frameon=True,
+ facecolor="white",
+ framealpha=1,
+ title="Post Mortem Scope, " + self.t0_pmlpd64,
+ )
+ fig.subplots_adjust(hspace=0)
+
+ return fig
diff --git a/services/postmortemlog/current/app/uv.lock b/services/postmortemlog/current/app/uv.lock
index d9cb60f..284dc68 100644
--- a/services/postmortemlog/current/app/uv.lock
+++ b/services/postmortemlog/current/app/uv.lock
@@ -30,6 +30,9 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "agebd" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "urllib3" },
]
[package.dev-dependencies]
@@ -40,7 +43,12 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "agebd", editable = "../../../../packages/agebd" }]
+requires-dist = [
+ { name = "agebd", editable = "../../../../packages/agebd" },
+ { name = "matplotlib", specifier = ">=3.10.9" },
+ { name = "numpy", specifier = ">=2.0.2" },
+ { name = "urllib3", specifier = ">=2.7.0" },
+]
[package.metadata.requires-dev]
dev = [
@@ -67,6 +75,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
+ { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
[[package]]
name = "exceptiongroup"
version = "1.3.1"
@@ -79,6 +120,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
+[[package]]
+name = "fonttools"
+version = "4.63.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" },
+ { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" },
+ { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
+]
+
[[package]]
name = "h5py"
version = "3.16.0"
@@ -106,6 +164,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
+[[package]]
+name = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" },
+ { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" },
+ { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" },
+ { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" },
+]
+
[[package]]
name = "markdown-it-py"
version = "4.2.0"
@@ -118,6 +202,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" },
+ { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" },
+ { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" },
+]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -158,6 +270,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
+[[package]]
+name = "pillow"
+version = "12.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
+ { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
+ { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
+ { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -216,6 +345,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
[[package]]
name = "rich"
version = "15.0.0"
@@ -263,6 +404,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
[[package]]
name = "tomli"
version = "2.4.1"
@@ -295,3 +445,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]