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" }, +]