feat: migrate tune python service

This commit is contained in:
Benjamin Labrecque
2026-07-16 17:00:27 +02:00
parent f07e2e8a0a
commit 5d038c17fe
3 changed files with 457 additions and 55 deletions
+4
View File
@@ -5,6 +5,10 @@ description = "AGEBD-TUNE Service"
requires-python = "==3.10.*"
dependencies = [
"agebd",
"nafflib>=2.1.1",
"numpy>=2.2.6",
"scikit-learn>=1.7.2",
"scipy>=1.15.3",
]
[dependency-groups]
@@ -1,12 +1,20 @@
import pickle # ONLY FOR TESTING
import time
from time import sleep, time
from epics import dbr
import nafflib
import numpy as np
from epics import PV
from agebd.pv import LocalPVLink
# TODO: handle how to incorporate this into the framework
from OMFacility import Facility
from scipy.fft import fft
from sklearn.utils.extmath import randomized_svd
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__)
@@ -14,36 +22,79 @@ PV = get_pv_class()
class PVs(BasePVs):
# Option 1:
## Service specific PVs from dedicated IOC
my_pv1 = PV("AGEBD-TUNE:AO") # TODO: is this a good PV to use?
Q1_fft = PV("AGEBD-TUNE:HORIZONTAL-PEAK1-FREQ") # horizontal tune from FFT
Q2_fft = PV("AGEBD-TUNE:VERTICAL-PEAK1-FREQ") # vertical tune from FFT
Q1_naff = PV("AGEBD-TUNE:Q1_NAFF") # horizontal tune from NAFF
Q2_naff = PV("AGEBD-TUNE:Q2_NAFF") # vertical tune from NAFF
## Service specific PVs from other IOCs
# ...
roi_Xi = PV("AGEBD-TUNE:CONTROL-ROI-Xi")
roi_Xf = PV("AGEBD-TUNE:CONTROL-ROI-Xf")
roi_Yi = PV("AGEBD-TUNE:CONTROL-ROI-Yi")
roi_Yf = PV("AGEBD-TUNE:CONTROL-ROI-Yf")
TbT_firstIndex = PV("AGEBD-TUNE:CONTROL-TbTFirstIndex")
TbT_lastIndex = PV("AGEBD-TUNE:CONTROL-TbTLastIndex")
specx_all = PV("AGEBD-TUNE:HORIZONTAL-SPEC-ALL")
specy_all = PV("AGEBD-TUNE:VERTICAL-SPEC-ALL")
specx_roi = PV("AGEBD-TUNE:HORIZONTAL-SPEC-ROI")
specy_roi = PV("AGEBD-TUNE:VERTICAL-SPEC-ROI")
osc_sample_all = PV("AGEBD-TUNE:OSCILLATION-SAMPLE-ALL")
spec_sample_all = PV("AGEBD-TUNE:SPEC-SAMPLE-ALL")
# to be implemented by Felix
measTime = PV("AGEBD-TUNE:MEAS-TIME") # time taken for measurement
NAveraging = PV("AGEBD-TUNE:CONTROL-NAveraging")
naff_compute = PV("AGEBD-TUNE:CONTROL-NAFF")
naff_harmonics = PV("AGEBD-TUNE:NAFF-HARMONICS")
BPMSelect_single = PV("AGEBD-TUNE:SINGLE-BPM") # select whether only a single BPM is to be used
BPMSelect_single_selected = PV("AGEBD-TUNE:BPM-SELECT-SINGLE")
SVDclean = PV("AGEBD-TUNE:SVD-CLEAN") # select whether to use SVD cleaning
SVDclean_NEig = PV("AGEBD-TUNE:SVD-CLEAN-Neigenvalues") # select whether to use SVD cleaning
osc_x = PV("AGEBD-TUNE:OSCILLATION-X-ALL") # horizontal TbT to be transmissed
osc_y = PV("AGEBD-TUNE:OSCILLATION-Y-ALL") # vertical TbT to be transmissed
# usually it is advisable to trigger the mainloop of a service on changes of certain PVs:
BPMTriggerEvent = PV("ARS07-CECL-DBPM3-1:EEVR-EVENT0-ID")
CallbackPV_24 = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Ext-EVRCount-I")
CallbackPV_25 = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Ext-BPM-EVRCount-I")
CallbackPV_31 = PV("AGETI-CVME-MASTER-TMA:Evt-SR-Pinger-H-EVRCount-I")
CallbackPV_32 = PV("AGETI-CVME-MASTER-TMA:Evt-SR-Inj-EVRCount-I")
CallbackPV_34 = PV("AGETI-CVME-MASTER-TMA:Evt-SR-FKICK-EVRCount-I")
CallbackPV_40 = PV("AGETI-CVME-MASTER-TMA:Evt-SR-BPM-EVRCount-I")
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
S2 = Facility("/sls/bd/bin/MasterLayout.txt")
S2.Layout("full", -1)
# Option 2:
## Service specific PVs from dedicated IOC
self.my_pv2 = PV(
"AGEBD-TUNE:BO"
) # TODO: is this a good PV to use?
disabledBPMs = ["ARS01-DBPM-0530", "ARS03-DBPM-0840", "ARS10-DBPM-0970", "ARS11-DBPM-0880"]
BPMNames_BPM = S2.listElement_TYPE("BPM")
BPMNames_BPMA = S2.listElement_TYPE("BPMA")
BPMNames = [item for item in BPMNames_BPM if item not in disabledBPMs]
BPMNamesA = [item for item in BPMNames_BPMA if item not in disabledBPMs]
## Service specific PVs from other IOCs
# ...
BPMNames_x = [name + ":DAQ-BEAM-STG0-X" for name in BPMNames]
BPMNames_y = [name + ":DAQ-BEAM-STG0-Y" for name in BPMNames]
BPMNames_I = [name + ":DAQ-BEAM-STG0-Q" for name in BPMNames]
BPMNamesA_x = [name + ":DAQ-BEAM-STG0-X" for name in BPMNamesA]
BPMNamesA_y = [name + ":DAQ-BEAM-STG0-Y" for name in BPMNamesA]
BPMNamesA_I = [name + ":DAQ-BEAM-STG0-Q" for name in BPMNamesA]
# 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-TUNE:CALLBACK", auto_monitor=dbr.DBE_VALUE
) # TODO: ok?
self.BPMx = [PV(name) for name in BPMNames_x]
self.BPMy = [PV(name) for name in BPMNames_y]
self.BPMI = [PV(name) for name in BPMNames_I]
self.BPMxA = [PV(name) for name in BPMNamesA_x]
self.BPMyA = [PV(name) for name in BPMNamesA_y]
self.BPMIA = [PV(name) for name in BPMNamesA_I]
self.CallbackPV = None
class Service(BaseService[PVs]):
@@ -56,38 +107,219 @@ class Service(BaseService[PVs]):
):
super().__init__(name, pvs, version, sleep_interval)
# TODO: What is this for?
self.path = "sls/bd/bin"
self.update_control()
def update_control(self):
pvs = self.pvs
BPMTriggerEvent = pvs.BPMTriggerEvent.get()
if BPMTriggerEvent == 24: # trigger is booster extraction
pvs.CallbackPV = pvs.CallbackPV_24
elif BPMTriggerEvent == 25: # trigger is booster extraction BPM
pvs.CallbackPV = pvs.CallbackPV_25
elif BPMTriggerEvent == 31: # trigger is pingerH event
pvs.CallbackPV = pvs.CallbackPV_31
elif BPMTriggerEvent == 32: # trigger is SR-Inj event
pvs.CallbackPV = pvs.CallbackPV_32
elif BPMTriggerEvent == 34: # trigger is pingerV event fast kicker event
pvs.CallbackPV = pvs.CallbackPV_34
elif BPMTriggerEvent == 40: # trigger is SR BPM Sync event
pvs.CallbackPV = pvs.CallbackPV_40
else:
print("BPM trigger event ID not understood. Value is: " + str(BPMTriggerEvent))
samplingRate = 2 # sampling rate of BPMs w.r.t. 1 MHz
TbT_firstIndex = np.int64(pvs.TbT_firstIndex.get())
TbT_lastIndex = np.int64(pvs.TbT_lastIndex.get())
# user was stupid: set first index higher than last index
if TbT_firstIndex >= TbT_lastIndex:
TbT_firstIndex = TbT_lastIndex - 1
pvs.TbT_firstIndex.put(TbT_firstIndex)
# user was stupid: set last index lower than first index
if TbT_lastIndex <= TbT_firstIndex:
TbT_lastIndex = TbT_firstIndex + 1
pvs.TbT_lastIndex.put(TbT_lastIndex)
self.Index_first = np.int64(
np.arange(TbT_firstIndex * samplingRate, TbT_lastIndex * samplingRate + 1, samplingRate)
)
self.Index_second = np.int64(
np.arange(
1 + TbT_firstIndex * samplingRate,
TbT_lastIndex * samplingRate + 1 + 1,
samplingRate,
)
)
self.NIndex = len(range(TbT_firstIndex, TbT_lastIndex + 1))
# NAFF control parameters
self.naff_onoff = pvs.naff_compute.get()
self.naff_Nharmonics = int(pvs.naff_harmonics.get())
pvs.osc_sample_all.put(np.int64(np.arange(self.NIndex)))
pvs.spec_sample_all.put(np.linspace(0, 1, self.NIndex))
if pvs.BPMSelect_single.get():
BPMNames_single = pvs.BPMSelect_single_selected.get()
index_BPM = next((i for i, s in enumerate(BPMNames) if s == BPMNames_single), None)
index_BPMA = next((i for i, s in enumerate(BPMNamesA) if s == BPMNames_single), None)
if index_BPM is not None:
pvs.BPMx_selected = [pvs.BPMx[index_BPM]]
pvs.BPMy_selected = [pvs.BPMy[index_BPM]]
pvs.BPMI_selected = [pvs.BPMI[index_BPM]]
else:
pvs.BPMx_selected = [pvs.BPMxA[index_BPMA]]
pvs.BPMy_selected = [pvs.BPMyA[index_BPMA]]
pvs.BPMI_selected = [pvs.BPMIA[index_BPMA]]
self.NBPMs = 1
else:
pvs.BPMx_selected = pvs.BPMx
pvs.BPMy_selected = pvs.BPMy
pvs.BPMI_selected = pvs.BPMI
self.NBPMs = len(pvs.BPMx)
self.NSamples = int(pvs.NAveraging.get())
self.N_SVDeig = int(pvs.SVDclean_NEig.get())
def update(self):
# when callback is fired, this code will be executed
pvs = self.pvs
if self.CallbackFired:
#######################################################################################
#######################################################################################
# here goes all the code that your service
# is supposed to be running when the callback is fired
time.sleep(1)
#######################################################################################
#######################################################################################
t_start = time()
self.update_control()
if self.NBPMs == 0:
return
x = np.zeros((self.NBPMs, self.NIndex, self.NSamples))
y = np.zeros((self.NBPMs, self.NIndex, self.NSamples))
for sample in range(self.NSamples):
if sample > 0:
sleep(1 / 3)
xtmp = np.array([pv.get() for pv in pvs.BPMx_selected])
ytmp = np.array([pv.get() for pv in pvs.BPMy_selected])
Itmp = np.array([pv.get() for pv in pvs.BPMI_selected])
index_max = np.argmax(Itmp[:, 0:2], axis=1)
for n in range(self.NBPMs):
if index_max[n] == 0:
x[n, :, sample] = xtmp[n, self.Index_first] - np.mean(
xtmp[n, self.Index_first]
) # subtract mean values
y[n, :, sample] = ytmp[n, self.Index_first] - np.mean(
ytmp[n, self.Index_first]
) # subtract mean values
else:
x[n, :, sample] = xtmp[n, self.Index_second] - np.mean(
xtmp[n, self.Index_second]
) # subtract mean values
y[n, :, sample] = ytmp[n, self.Index_second] - np.mean(
ytmp[n, self.Index_second]
) # subtract mean values
Ax = np.mean(x, axis=2) # average over NSamples
Ay = np.mean(y, axis=2)
if pvs.SVDclean.get():
Ux, Sx, Vx_T = randomized_svd(Ax, n_components=self.N_SVDeig)
Ax = Ux @ np.diag(Sx) @ Vx_T
Uy, Sy, Vy_T = randomized_svd(Ay, n_components=self.N_SVDeig)
Ay = Uy @ np.diag(Sy) @ Vy_T
Fx = fft(Ax[0, :]) # FFT first BPM only
Fy = fft(Ay[0, :])
specx = np.abs(Fx) # Take absolute of FFT to obtain amplitude spectra
specy = np.abs(Fy)
specx[: np.int64(np.ceil(self.NIndex * 0.01))] = 0 # remove DC component
specy[: np.int64(np.ceil(self.NIndex * 0.01))] = 0 # remove DC component
specx_avg = specx / np.max(specx) # normalize spectra of first BPM
specy_avg = specy / np.max(specy) # normalize spectra of first BPM
window_x_start = round(pvs.roi_Xi.get() * self.NIndex)
window_x_end = round(pvs.roi_Xf.get() * self.NIndex)
window_y_start = round(pvs.roi_Yi.get() * self.NIndex)
window_y_end = round(pvs.roi_Yf.get() * self.NIndex)
window_indices_x = np.arange(window_x_start, window_x_end + 1)
window_indices_y = np.arange(window_y_start, window_y_end + 1)
index_max_local_x = np.argmax(specx_avg[window_x_start:window_x_end])
index_max_local_y = np.argmax(specy_avg[window_y_start:window_y_end])
Q1_FFTval = window_indices_x[index_max_local_x] / self.NIndex
Q2_FFTval = window_indices_y[index_max_local_y] / self.NIndex
pvs.Q1_fft.put(Q1_FFTval)
pvs.Q2_fft.put(Q2_FFTval)
Ax_mixed = Ax.transpose()
Ay_mixed = Ay.transpose()
Ax_mixed = Ax_mixed.flatten()
Ay_mixed = Ay_mixed.flatten()
Q1_naff = np.nan
Q2_naff = np.nan
if self.naff_onoff:
h1_naff, _, _ = nafflib.get_tunes(Ax_mixed, self.naff_Nharmonics)
h2_naff, _, _ = nafflib.get_tunes(Ay_mixed, self.naff_Nharmonics)
h1_naff = h1_naff * self.NBPMs
h2_naff = h2_naff * self.NBPMs
Q1_naff = np.mod(h1_naff, 1)
Q2_naff = np.mod(h2_naff, 1)
Q1_naff[Q1_naff > 0.5] = (
1 - Q1_naff[Q1_naff > 0.5]
) # above-half integer peak found. Going down.
Q2_naff[Q2_naff > 0.5] = 1 - Q2_naff[Q2_naff > 0.5]
Q1_naff = Q1_naff[(Q1_naff >= Q1_FFTval - 0.01) & (Q1_naff <= Q1_FFTval + 0.01)]
Q2_naff = Q2_naff[(Q2_naff >= Q2_FFTval - 0.01) & (Q2_naff <= Q2_FFTval + 0.01)]
if len(Q1_naff) == 0:
Q1_naff = np.nan
else:
Q1_naff = np.mean(Q1_naff)
if len(Q2_naff) == 0:
Q2_naff = np.nan
else:
Q2_naff = np.mean(Q2_naff)
# if self.NBPMs == 1:
# integerX = 0
# integerY = 0
# else:
# integerX = 39
# integerY = 15
# dh1 = h1_naff*self.NBPMs-(integerX+Q1_FFTval)
# dh2 = h2_naff*self.NBPMs-(integerY+Q2_FFTval)
# if min(abs(dh1))>0.5:# correct integer not found. Increasing number of harmonics
# h1_naff, _, _ = nafflib.get_tunes(Ax_mixed,50)
# dh1 = h1_naff*self.NBPMs-(integerX+Q1_FFTval)
# if min(abs(dh2))>0.5:# correct integer not found. Increasing number of harmonics
# h2_naff, _, _ = nafflib.get_tunes(Ay_mixed,25)
# dh2 = h2_naff*self.NBPMs-(integerY+Q2_FFTval)
# Q1_naff = h1_naff[np.argmin(abs(dh1))]*self.NBPMs
# Q2_naff = h2_naff[np.argmin(abs(dh2))]*self.NBPMs
pvs.Q1_naff.put(Q1_naff)
pvs.Q2_naff.put(Q2_naff)
pvs.specx_all.put(specx_avg)
pvs.specy_all.put(specy_avg)
pvs.specx_roi.put(specx_avg[window_x_start - 1 : window_x_end])
pvs.specy_roi.put(specy_avg[window_y_start - 1 : window_y_end])
pvs.osc_x.put(Ax[0, :])
pvs.osc_y.put(Ay[0, :])
t_end = time()
pvs.measTime.put((t_end - t_start) * 1e3)
# callback done
self.CallbackFired = 0
#######################################################################################
#######################################################################################
# here goes all the code that your service
# is supposed to be running in any case, e.g.,
# 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")
# get some pvs/vals
self.val1 = self.pvs.my_pv1.get()
self.val2 = self.pvs.my_pv2.get()
# maybe sleep to limit the update loop execution rate
time.sleep(1)
#######################################################################################
#######################################################################################
+167 -1
View File
@@ -30,6 +30,10 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "agebd" },
{ name = "nafflib" },
{ name = "numpy" },
{ name = "scikit-learn" },
{ name = "scipy" },
]
[package.dev-dependencies]
@@ -40,7 +44,13 @@ dev = [
]
[package.metadata]
requires-dist = [{ name = "agebd", editable = "../../../../packages/agebd" }]
requires-dist = [
{ name = "agebd", editable = "../../../../packages/agebd" },
{ name = "nafflib", specifier = ">=2.1.1" },
{ name = "numpy", specifier = ">=2.2.6" },
{ name = "scikit-learn", specifier = ">=1.7.2" },
{ name = "scipy", specifier = ">=1.15.3" },
]
[package.metadata.requires-dev]
dev = [
@@ -106,6 +116,27 @@ 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 = "joblib"
version = "1.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
[[package]]
name = "llvmlite"
version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/a0/acc8ffcd5bdc63df0097e22c719bfcd61b604358343089313a8aebbb24ab/llvmlite-0.48.0.tar.gz", hash = "sha256:543b19f9ef8f3c7c60d1468191e4ee1b1537bf9f8a3d56f64c0ddd98de92edd2", size = 184016, upload-time = "2026-07-02T20:20:05.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/4e/32543c42568fb321b3bdfcf9106e4116ab8f5a7bbcfd9ecf5569b0c07d83/llvmlite-0.48.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:614aad57df707e3172efd5165f2aa7da6a0c6897e40dce590bf756396815ba76", size = 40480650, upload-time = "2026-07-01T18:41:01.945Z" },
{ url = "https://files.pythonhosted.org/packages/a9/0d/6aa48abd423067139a129d1434b77bbcc56080db51d12a88510bb491ca3d/llvmlite-0.48.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13532f248960ba888ad5ab8150494e2f3a3d20e5f59f264e63741ea5b0ba844c", size = 59890118, upload-time = "2026-07-01T18:41:10.608Z" },
{ url = "https://files.pythonhosted.org/packages/5a/c7/aa917444d871a79608af49149de1b28764e87d2ab41f933c5cd02431d03d/llvmlite-0.48.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee0c77685a18f5fca994ae21d0763007fca5c5c64b41de37accc78b69079176", size = 58343459, upload-time = "2026-07-01T18:41:06.21Z" },
{ url = "https://files.pythonhosted.org/packages/c5/2b/ceee1cdc263617109d514ac4d1b31f10a282662740ff7d5777baae25b3b5/llvmlite-0.48.0-cp310-cp310-win_amd64.whl", hash = "sha256:02853fe4214acb3780fc920c3fee10564b61d58a35e1b78afcc8a546c2deaba3", size = 41864734, upload-time = "2026-07-01T18:41:14.746Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
@@ -127,6 +158,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "nafflib"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numba" },
{ name = "numpy" },
{ name = "pandas" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/0d/3d563dfc173916d5c0cc1fe614cdf312877540b9cea5bcd50a271ba5f6a3/nafflib-2.1.1.tar.gz", hash = "sha256:e4c67224c01db4b694d8f9c73c5b878178d80379ad99ae89adb4cce21ededd9b", size = 22926, upload-time = "2025-11-22T15:47:22.152Z" }
[[package]]
name = "numba"
version = "0.66.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "llvmlite" },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/a0/570e3dc53e5602b49108f62a13e529f1eec8bfc7ef37d49c825924dcf546/numba-0.66.0.tar.gz", hash = "sha256:b900e63a0e26c05ea9a6d5a3a5a0a177cb64c5011887bf43edb8c3ed2c38d363", size = 2806181, upload-time = "2026-07-01T23:12:46.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/48/d139bde40f2359351bfe26ee1b261937f458ac177ab810d4f045ae1c9d92/numba-0.66.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:27951c47e0def9bf8afe580eb961102902e2fd23cb77924b7d9d7cc0f8b444cb", size = 2727368, upload-time = "2026-07-01T23:12:04.282Z" },
{ url = "https://files.pythonhosted.org/packages/36/e4/b780bfa9191410da50ba249cb3248a75014e17f611e72709cbddcb21f42d/numba-0.66.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc408c54b450f41582f4be1608f8981c1dcc44c7f40355cc150dd93015753407", size = 3803554, upload-time = "2026-07-01T23:12:06.379Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b2/a051b96626bdf5c4d8fa6b8d450605c09638d85dc872ab63ef9a67096dca/numba-0.66.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c14c044c06b453ec3fa7715dfe75425e2ba72c73377a7ffde6d9ec511dfd94c", size = 3510065, upload-time = "2026-07-01T23:12:08.051Z" },
{ url = "https://files.pythonhosted.org/packages/34/01/24dcdc3e919522e2efbd92969c281ff40deb1d5f8a994bcd0057081c158c/numba-0.66.0-cp310-cp310-win_amd64.whl", hash = "sha256:2338cc0d43609fe448930848fd35a5bc688761b986f81b597a6f45cc0f8c9577", size = 2780379, upload-time = "2026-07-01T23:12:09.772Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
@@ -158,6 +216,27 @@ 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 = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
{ url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
{ url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
{ url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
{ url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
{ url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -216,6 +295,27 @@ 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 = "pytz"
version = "2026.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
@@ -254,6 +354,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
]
[[package]]
name = "scikit-learn"
version = "1.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "joblib" },
{ name = "numpy" },
{ name = "scipy" },
{ name = "threadpoolctl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" },
{ url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" },
{ url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" },
{ url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" },
{ url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" },
]
[[package]]
name = "scipy"
version = "1.15.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" },
{ url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" },
{ url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" },
{ url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" },
{ url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" },
{ url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" },
{ url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" },
{ url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@@ -263,6 +402,24 @@ 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 = "threadpoolctl"
version = "3.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
@@ -295,3 +452,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 = "tzdata"
version = "2026.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
]