From 5d038c17fe0986b910a85ca31a3e35f611b2eb7a Mon Sep 17 00:00:00 2001 From: Benjamin Labrecque Date: Thu, 16 Jul 2026 17:00:27 +0200 Subject: [PATCH] feat: migrate tune python service --- services/tune/current/app/pyproject.toml | 4 + .../current/app/src/agebd_tune/service.py | 340 +++++++++++++++--- services/tune/current/app/uv.lock | 168 ++++++++- 3 files changed, 457 insertions(+), 55 deletions(-) diff --git a/services/tune/current/app/pyproject.toml b/services/tune/current/app/pyproject.toml index 0c62707..c501328 100644 --- a/services/tune/current/app/pyproject.toml +++ b/services/tune/current/app/pyproject.toml @@ -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] diff --git a/services/tune/current/app/src/agebd_tune/service.py b/services/tune/current/app/src/agebd_tune/service.py index be08b40..2c9945a 100644 --- a/services/tune/current/app/src/agebd_tune/service.py +++ b/services/tune/current/app/src/agebd_tune/service.py @@ -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) - ####################################################################################### - ####################################################################################### diff --git a/services/tune/current/app/uv.lock b/services/tune/current/app/uv.lock index 62436c8..8ea8b91 100644 --- a/services/tune/current/app/uv.lock +++ b/services/tune/current/app/uv.lock @@ -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" }, +]