diff --git a/pyproject.toml b/pyproject.toml index 9145e85..b3d8f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,9 @@ license-files = ["LICEN[CS]E*"] dependencies = [ "matplotlib>=3.10.8", "numpy>=2.4.2", + "pydantic>=2.12.5", "scipy>=1.17.1", + "toml>=0.10.2", "tqdm>=4.67.3", ] dynamic = ["version"] @@ -40,7 +42,7 @@ helena = "helena.helena:run" [project.optional-dependencies] tests = [ "pytest>=9.0.2", - "snaptol>=0.0.3", + "snaptol>=0.0.4", ] lint = [ "ruff>=0.15.4", diff --git a/src/helena/helena.py b/src/helena/helena.py index 5a13d93..ec70027 100644 --- a/src/helena/helena.py +++ b/src/helena/helena.py @@ -30,6 +30,7 @@ def run(argv=None): if argv is None: argv = sys.argv[1:] (options, args) = parser.parse_args(argv) + input_file = "input.toml" if len(args) == 0 else args[0] if 'True' in str(options): import os, sys @@ -70,6 +71,8 @@ def run(argv=None): import csv import re import gc + import toml + from typing import TypeVar, Callable #Enforce matplotlib to avoid instancing undisplayed windows #matplotlib-tcl-asyncdelete-async-handler-deleted-by-the-wrong-thread @@ -85,222 +88,467 @@ def run(argv=None): from matplotlib import ticker from scipy import ndimage from tqdm import tqdm + from pydantic import Field + from pydantic.dataclasses import dataclass #from Read_data_functions.py import overlay_GEC_geometry + #Commonly used variable sets. + variable_sets = { + "Phys": ['E','S-E','SEB-E','TE','PPOT','P-POT','POW-RF','POW-RF-E','POW-ICP','POW-ICP1','POW-ICP2','POW-ICP3', + 'POW-ICP4','POW-ALL','EB-ESORC','COLF','SIGMA','EF-TOT', 'ERADIAL','ETHETA','EAXIAL','PHASEER','PHASE', + 'PHASEEZ','EAMB-Z','EAMB-R','RHO','BR','BRS','BZ','BZS','BT','BTS','BRF', 'PHASEBR','PHASEBT', + 'PHASEBZ','VR-ION+','VZ-ION+','E FLUX-R','FR-E','E FLUX-Z','FZ-E','JZ-NET','JR-NET','J-THETA', + 'J-TH(MAG)','J-TH(PHA)','PRESSURE','TG-AVE','VR-NEUTRAL','VZ-NEUTRAL'], + "PhysCoilsEF": ['ERADIAL-2','ETHETA-2','EAXIAL-2','PHASEER-2','PHASEEZ-2','ERADIAL-3','ETHETA-3','EAXIAL-3', + 'PHASEER-3','PHASEEZ-3','ERADIAL-4','ETHETA-4','EAXIAL-4','PHASEER-4','PHASEEZ-4','ERADIAL-5', + 'ETHETA-5','EAXIAL-5','PHASEER-5','PHASEEZ-5','ERADIAL-6','ETHETA-6','EAXIAL-6','PHASEER-6', + 'PHASEEZ-6','ERADIAL-7','ETHETA-7','EAXIAL-7','PHASEER-7','PHASEEZ-7','ERADIAL-8','ETHETA-8', + 'EAXIAL-8','PHASEER-8','PHASEEZ-8'], + "PhysCoilsBF": ['BT-2','BT-3','BT-4','BT-5','BT-6','BT-7','BT-8','BRF-2','BRF-3','BRF-4','BRF-5','BRF-6', + 'BRF-7','BRF-8','PHASEBT-2','PHASEBT-3','PHASEBT-4','PHASEBT-5','PHASEBT-6','PHASEBT-7', + 'PHASEBT-8'], + + "Conv": ['E','TE','PPOT','POW-RF','SIGMA','EF-TOT','TG-AVE'], + + "TEST": ['AR2+'], + "Ar": ['AR3S','AR4SM','AR4SR','AR4SPM','AR4SPR','AR4P','AR4D','AR','AR+','AR2+','AR2*','S-AR+','S-AR4P', + 'SEB-AR+','SEB-AR4P','FZ-AR3S','FR-AR3S','FR-AR+','FZ-AR+','FZ-AR3S','FR-AR3S'], + "O2": ['O3','O2','O2V','O2*','O2*1S','O2+','O2-','O','O1S','O+','O-','O*','S-O3','S-O2+','S-O+','S-O-','SEB-O3', + 'SEB-O+','SEB-O2+','SEB-O-','FR-O+','FZ-O+','FR-O-','FZ-O-'], + "H2": ['H2V0','H2V1','H2V2','H2V3','H1','H*','H**','H2+','H+','H-','S-H+','SEB-H+','S-2H+','SEB-2H+','S-H-', + 'SEB-H-','FZ-H2V0','FR-H2V0','FZ-H1','FR-H1','FZ-H+','FR-H+','FZ-H2+','FR-H2+','FZ-H-','FR-H-'], + "N2": ['N2','N2V','N2*','N2**','N2+','N','N*','N+'], + "Cl": ['Cl2','Cl','CL+','CL-','Cl2V','Cl2+','CL*','CL**','CL***'], + "F": ['F2','F2*','F2+','F','F*','F+','F-','S-F','S-F+','S-F-','SEB-F','SEB-F+','SEB-F-','FZ-F','FR-F','FZ-F+', + 'FR-F+','FZ-F-','FR-F-','FZ-F+','FR-F+'], + "H2O": ['H2O','H2O+','OH','OH-','H2OV','H2O2','S-H2O','SEB-H2O','S-H2OV','SEB-H2OV','S-H2O+','SEB-H2O+','S-OH', + 'SEB-OH','S-OH-','SEB-OH-','S-OH+','SEB-OH+'], + "COx": ['CO2','CO2V','CO+','CO','CO+','C','C+'], + "CHx": ['CH4','CH3','CH2','CH','C','CH5+','CH4+','CH3+','CH2+','CH+','C+'], + "NHx": ['NH3','NH2','NH','NH4+','NH3+','NH2+','NH+'], + "NOx": ['NO2','NO2+','N2O','N2O+','NO','NO+'], + "NFx": ['NF3A','NF2A','NFA','NF3B','NF2B','NFB','NF3+','NF2+','NF+'], + "SFx": ['SF6','SF5','SF4','SF3','SF2','SF','S','SF5+','SF4+','SF3+','SF2+','SF+','S+','SF6-','SF5-'], + "Al": ['AL','AL*','AL**','AL+','S-AL','SEB-AL','S-AL*','SEB-AL*','S-AL**','SEB-AL**','S-AL+','SEB-AL+','FZ-AL+', + 'FR-AL+'], + "Be": ['BE','BE1','BE2','BE3','BE4','BE5','BE6','BE7','BE8','BE9','BE+','FR-BE','FZ-BE','FR-BE+','FZ-BE+'], + + "Ar_Phase": ['S-E','S-AR+','S-AR4P','SEB-E','SEB-AR+','SEB-AR4P','SRCE-2437','FR-E','FZ-E','TE','PPOT', + 'POW-ALL'], + "O2_Phase": ['S-E','S-O+','S-O-','S-O2+','SEB-O+','SEB-O-','SEB-O2+','S-O3P3P','SEB-O3P3P','TE','PPOT','FR-E', + 'FZ-E'], + + "PRCCPAr_PCMC": ['AR^0.35','EB-0.35','ION-TOT0.35'], + "PRCCPO2_PCMC": ['O^0.35','EB-0.35','ION-TOT0.35'], + "GECCCP2a_BE_PCMC": ['AR^ 2.6 L','BE^ 2.6 L','EB- 2.6 L'], + } + + + _T = TypeVar("T") + + + @dataclass + class ChemistryInput: + variables: list[str] = Field(default_factory=lambda: variable_sets["Phys"] + variable_sets["Ar"]) + multivar: list[str] = Field(default_factory=list) + radial_profiles: list[int] = Field(default_factory=list) + axial_profiles: list[int] = Field(default_factory=lambda: [0]) + probe_loc: list[int] = Field(default_factory=list) + + # EDF. + IEDF_variables: list[str] = Field(default_factory=lambda: variable_sets["GECCCP2a_BE_PCMC"].copy()) + NEDF_variables: list[str] = Field(default_factory=list) + + # Movie. + phase_variables: list[str] = Field(default_factory=lambda: variable_sets["Ar_Phase"].copy()) + electrode_loc: list[int] = Field(default_factory=lambda: [0, 0]) + waveform_locs: list[int] = Field(default_factory=list) + + # Diagnostic. + sheath_ROI: list[int] = Field(default_factory=list) + source_width: list[int] = Field(default_factory=list) + + def __post_init__(self): + self.variables = self.expand_variable_set(self.variables) + self.multivar = self.expand_variable_set(self.multivar) + self.IEDF_variables = self.expand_variable_set(self.IEDF_variables) + self.NEDF_variables = self.expand_variable_set(self.NEDF_variables) + self.phase_variables = self.expand_variable_set(self.phase_variables) + + @staticmethod + def expand_variable_set(items: list[str]) -> list[str]: + out = [] + + for item in items: + if item in variable_sets: + out.extend(variable_sets[item]) + else: + out.append(item) + + return out + + @dataclass + class FiguresInput: + tecplot2D: bool = False + + # Movie. + movieicp2D: bool = False + movieicp1D: bool = False + timeaxis1D: bool = False + convergence: bool = False + iterstep: int = 1 + + # Profiles. + monoprofiles: bool = False + multiprofiles: bool = False + compare_profiles: bool = False + + # Trends. + trend_phase_averaged: bool = False + trend_phase_resolved: bool = False + thrust_loc: int = 45 + + # Resolve. + phase_resolve2D: bool = False + phase_resolve1D: bool = False + sheath_dynamics: bool = False + PROES: bool = False + phase_cycles: float = 1.01 + do_Fwidth: int = 0 + + # EDF. + IEDF_angular: bool = False + IEDF_trends: bool = False + EEDF: bool = False + + @dataclass + class WriteInput: + ASCII: bool = False + CSV: bool = True + + @dataclass + class PrintoutInput: + general_trends: bool = False + Knudsen_number: bool = False + total_power: bool = False + Reynolds: bool = False + DC_bias: bool = False + thrust: bool = False + sheath: bool = False + + @dataclass + class ImageInput: + extension: str = ".png" + + # Style. + interp: str = "spline36" + cmap: str = "plasma" + + # Geometry. + aspect_ratio: list[float] = Field(default_factory=lambda: [10.0, 10.0]) + radial_crop: list[float] = Field(default_factory=list) + axial_crop: list[float] = Field(default_factory=list) + rotate: bool = False + + # Display. + plot_symmetry: bool = False + plot_mesh: bool = True + plot_grid: bool = False + + # Contours. + plot_colourfill: bool = True + plot_contours: bool = True + contour_lvls: int = 10 + + # Axes. + axis_ticks: bool = True + axis_labels: bool = True + legend_loc: str = 'best' + + # Colorbar. + cbar_ticks: bool = True + cbar_bins: int = 5 + cbar_limit: list[float] = Field(default_factory=list) + + # Vector. + plot_vector: bool = True + vector_density: float = 1.5 + vector_lw: float = 1.0 + + # Processing. + normalise: bool = False + log_plot: bool = False + + # Overlays. + plot_sheath: bool = False + plot_phase_waveform: bool = False + plot_overlay: bool = False + + + @dataclass + class OverridesInput: + title: list[str] = Field(default_factory=list) + legend: list[str] = Field(default_factory=list) + xaxis: list[str] = Field(default_factory=list) + xlabel: list[str] = Field(default_factory=list) + ylabel: list[str] = Field(default_factory=list) + + @dataclass + class ExpertInput: + # Debug. + magmesh: int = 1 + ffmpeg_movies: bool = False + IDEBUG: bool = False + + # Warnings. + ignore_div_zero: bool = True + ignore_empty_contours: bool = True + + # Numerical. + sheath_method: str = 'AbsDensity' + thrust_method: str = 'AxialMomentum' + mean_calculation: str = 'MeanFraction' + + # Overrides. + DC_bias_axis: str = 'Auto' + sheath_ion_species: list[str] = Field(default_factory=lambda: ['AR+']) + + # Filtering. + kinetic_filtering: bool = True + plot_kinetic_filtering: bool = False + sav_window: int = 25 + sav_poly_order: int = 3 + + # Misc. + sheath_ion_ratio_threshold: float = 1.03 + conv_azimuthal_phase: bool = True + EDF_threshold: float = 0.01 + units: str = 'SI' + + + @dataclass + class Config: + chemistry: ChemistryInput = Field(default_factory=ChemistryInput) + figures: FiguresInput = Field(default_factory=FiguresInput) + write: WriteInput = Field(default_factory=WriteInput) + printout: PrintoutInput = Field(default_factory=PrintoutInput) + image: ImageInput = Field(default_factory=ImageInput) + overrides: OverridesInput = Field(default_factory=OverridesInput) + expert: ExpertInput = Field(default_factory=ExpertInput) + + + def create_input(data: dict, category: str, output_obj: type[_T], sections: list[str] | None = None) -> _T: + # data: the whole toml data + # category: the specific category we are creating an input for + # output_obj: the category's output object + # sections: the additional sections of this category + + if sections is None: + sections = [] + + category_dict = data.get(category, {}) + + output_dict = {k: v for k, v in category_dict.items() if k not in sections} + + for section in sections: + output_dict.update(category_dict.get(section, {})) + + return output_obj(**output_dict) + + + def load_config(toml_dict): + chemistry_input = create_input(toml_dict, "chemistry", ChemistryInput, ["EDF", "movie", "diagnostic"]) + figures_input = create_input(toml_dict, "figures", FiguresInput, ["movie", "profiles", "trends", "resolve", "EDF"]) + write_input = create_input(toml_dict, "write", WriteInput) + printout_input = create_input(toml_dict, "print", PrintoutInput) + image_input = create_input(toml_dict, "image", ImageInput, ["style", "geometry", "display", "contours", "axes", "colorbar", "vector", "processing", "overlays"]) + overrides_input = create_input(toml_dict, "overrides", OverridesInput) + expert_input = create_input(toml_dict, "expert", ExpertInput, ["debug", "warnings", "numerical", "overrides", "filtering", "misc"]) + + return Config( + chemistry=chemistry_input, + figures=figures_input, + write=write_input, + printout=printout_input, + image=image_input, + overrides=overrides_input, + expert=expert_input, + ) + + with open(input_file, "r") as toml_file: + toml_dict = toml.load(toml_file) + + config = load_config(toml_dict) #====================================================================# #LOW LEVEL INPUTS# #====================================================================# #Various debug and streamlining options. - Magmesh = 1 #initmesh.exe magnification factor. (Obsolete - legacy) - ffmpegMovies = False #If False: Suppresses ffmpeg routines, saves RAM. - IDEBUG = False #Produces debug outputs for most diagnostics. + Magmesh = config.expert.magmesh #initmesh.exe magnification factor. (Obsolete - legacy) + ffmpegMovies = config.expert.ffmpeg_movies #If False: Suppresses ffmpeg routines, saves RAM. + IDEBUG = config.expert.IDEBUG #Produces debug outputs for most diagnostics. #Warning suppressions - np.seterr(divide='ignore', invalid='ignore') #Suppresses divide by zero errors - #Fix: "can't invoke "event" command: application has been destroyed" error with PROES images - #Fix: "Exception KeyError: KeyError(,)" error + if config.expert.ignore_div_zero: + np.seterr(divide='ignore', invalid='ignore') #Suppresses divide by zero errors + #Fix: "can't invoke "event" command: application has been destroyed" error with PROES images + #Fix: "Exception KeyError: KeyError(,)" error - warnings.filterwarnings("ignore", message="No contour levels were found within the data range.") - #Fix: Suppress above warning on plotting of empty contour plots + if config.expert.ignore_empty_contours: + warnings.filterwarnings("ignore", message="No contour levels were found within the data range.") + #Fix: Suppress above warning on plotting of empty contour plots #Numerical Calculation Methods: - GlobSheathMethod = 'AbsDensity' #Set Global Sheath Calculation Method. - #Choices: ('AbsDensity','IntDensity') - GlobThrustMethod = 'AxialMomentum' #Set Global Thrust Calculation Method. - #Choices:('ThermalVelocity','AxialMomentum') - GlobMeanCalculation = 'MeanFraction' #Definition of 'mean' EDF value - #Choices: ('MeanEnergy','MeanFraction') + GlobSheathMethod = config.expert.sheath_method #Set Global Sheath Calculation Method. + GlobThrustMethod = config.expert.thrust_method #Set Global Thrust Calculation Method. + GlobMeanCalculation = config.expert.mean_calculation #Definition of 'mean' EDF value #Overrides or 'fudge factors' for diagnostics - DCbiasaxis = 'Auto' #Force Direction Over Which DCBias is Calculated - #Choices:('Axial','Radial','Auto') - SheathIonSpecies = ['AR+'] #Force Sheath Ion Species (blank for auto) - #['AR+'] #['O+'] + DCbiasaxis = config.expert.DC_bias_axis #Force Direction Over Which DCBias is Calculated + SheathIonSpecies = config.expert.sheath_ion_species #Force Sheath Ion Species (blank for auto) #Ratio of electrons to ions that determines the edge of the sheath #Ideally this should be 1.00, but in practice it's slightly over 1 at lower resolutions - Sheath_IonRatio_Threshold = 1.03 + Sheath_IonRatio_Threshold = config.expert.sheath_ion_ratio_threshold #Data Filtering and Smoothing Methods: - KineticFiltering = True #Pre-fit kinetic data employing a SavGol filter - PlotKineticFiltering = False #Plot Filtered Profiles, or employ only in trends. - Glob_SavWindow, Glob_SavPolyOrder = 25, 3 #Window > FeatureSize, Polyorder ~= Smoothness + KineticFiltering = config.expert.kinetic_filtering #Pre-fit kinetic data employing a SavGol filter + PlotKineticFiltering = config.expert.plot_kinetic_filtering #Plot Filtered Profiles, or employ only in trends. + Glob_SavWindow = config.expert.sav_window #Window > FeatureSize + Glob_SavPolyOrder = config.expert.sav_poly_order #Polyorder ~= Smoothness #Apply azimuthal direction (phase) to relevant variables if true, else plot magnitude only - ConvAzimuthalPhase = True + ConvAzimuthalPhase = config.expert.conv_azimuthal_phase #Minimum plotted EDF energy fraction, cuts x-axis at index where :: f(e) = f(e)*EDF_Threshold #Note: IEDF/EEDF trends are only taken within range :: EDF_threshold < f(e) < 1.0 - EDF_Threshold = 0.01 # i.e. = 0.0 to plot all + EDF_Threshold = config.expert.EDF_threshold # i.e. = 0.0 to plot all #Define units for particular variables - Units = 'SI' #'SI','CGS' + Units = config.expert.units #'SI','CGS' # FUNCTION MATHS ASSUMES SI, CGS WILL GIVE INCORRECT RESULTS - #################### - - #Commonly used variable sets. - Phys = ['E','S-E','SEB-E','TE','PPOT','P-POT','POW-RF','POW-RF-E','POW-ICP','POW-ICP1','POW-ICP2','POW-ICP3','POW-ICP4','POW-ALL','EB-ESORC','COLF','SIGMA','EF-TOT', 'ERADIAL','ETHETA','EAXIAL','PHASEER','PHASE','PHASEEZ','EAMB-Z','EAMB-R','RHO','BR','BRS','BZ','BZS','BT','BTS','BRF', 'PHASEBR','PHASEBT','PHASEBZ','VR-ION+','VZ-ION+','E FLUX-R','FR-E','E FLUX-Z','FZ-E','JZ-NET','JR-NET','J-THETA','J-TH(MAG)','J-TH(PHA)','PRESSURE','TG-AVE','VR-NEUTRAL','VZ-NEUTRAL'] - PhysCoilsEF = \ - ['ERADIAL-2','ETHETA-2','EAXIAL-2','PHASEER-2','PHASEEZ-2','ERADIAL-3','ETHETA-3','EAXIAL-3','PHASEER-3','PHASEEZ-3', \ - 'ERADIAL-4','ETHETA-4','EAXIAL-4','PHASEER-4','PHASEEZ-4','ERADIAL-5','ETHETA-5','EAXIAL-5','PHASEER-5','PHASEEZ-5', \ - 'ERADIAL-6','ETHETA-6','EAXIAL-6','PHASEER-6','PHASEEZ-6','ERADIAL-7','ETHETA-7','EAXIAL-7','PHASEER-7','PHASEEZ-7', \ - 'ERADIAL-8','ETHETA-8','EAXIAL-8','PHASEER-8','PHASEEZ-8'] - PhysCoilsBF = \ - ['BT-2','BT-3','BT-4','BT-5','BT-6','BT-7','BT-8', \ - 'BRF-2','BRF-3','BRF-4','BRF-5','BRF-6','BRF-7','BRF-8', \ - 'PHASEBT-2','PHASEBT-3','PHASEBT-4','PHASEBT-5','PHASEBT-6','PHASEBT-7','PHASEBT-8'] - - Conv = ['E','TE','PPOT','POW-RF','SIGMA','EF-TOT','TG-AVE'] - - TEST = ['AR2+'] - Ar = ['AR3S','AR4SM','AR4SR','AR4SPM','AR4SPR','AR4P','AR4D','AR','AR+','AR2+','AR2*','S-AR+','S-AR4P','SEB-AR+','SEB-AR4P','FZ-AR3S','FR-AR3S','FR-AR+','FZ-AR+','FZ-AR3S','FR-AR3S'] - O2 = ['O3','O2','O2V','O2*','O2*1S','O2+','O2-','O','O1S','O+','O-','O*','S-O3','S-O2+','S-O+','S-O-','SEB-O3','SEB-O+','SEB-O2+','SEB-O-','FR-O+','FZ-O+','FR-O-','FZ-O-'] - H2 = ['H2V0','H2V1','H2V2','H2V3','H1','H*','H**','H2+','H+','H-','S-H+','SEB-H+','S-2H+','SEB-2H+','S-H-','SEB-H-','FZ-H2V0','FR-H2V0','FZ-H1','FR-H1','FZ-H+','FR-H+','FZ-H2+','FR-H2+','FZ-H-','FR-H-'] - N2 = ['N2','N2V','N2*','N2**','N2+','N','N*','N+'] - Cl = ['Cl2','Cl','CL+','CL-','Cl2V','Cl2+','CL*','CL**','CL***'] - F = ['F2','F2*','F2+','F','F*','F+','F-','S-F','S-F+','S-F-','SEB-F','SEB-F+','SEB-F-','FZ-F','FR-F','FZ-F+','FR-F+','FZ-F-','FR-F-','FZ-F+','FR-F+'] - H2O = ['H2O','H2O+','OH','OH-','H2OV','H2O2','S-H2O','SEB-H2O','S-H2OV','SEB-H2OV','S-H2O+','SEB-H2O+','S-OH','SEB-OH','S-OH-','SEB-OH-','S-OH+','SEB-OH+'] - COx = ['CO2','CO2V','CO+','CO','CO+','C','C+'] - CHx = ['CH4','CH3','CH2','CH','C','CH5+','CH4+','CH3+','CH2+','CH+','C+'] - NHx = ['NH3','NH2','NH','NH4+','NH3+','NH2+','NH+'] - NOx = ['NO2','NO2+','N2O','N2O+','NO','NO+'] - NFx = ['NF3A','NF2A','NFA','NF3B','NF2B','NFB','NF3+','NF2+','NF+'] - SFx = ['SF6','SF5','SF4','SF3','SF2','SF','S','SF5+','SF4+','SF3+','SF2+','SF+','S+','SF6-','SF5-'] - Al = ['AL','AL*','AL**','AL+','S-AL','SEB-AL','S-AL*','SEB-AL*','S-AL**','SEB-AL**','S-AL+','SEB-AL+','FZ-AL+','FR-AL+'] - Be = ['BE','BE1','BE2','BE3','BE4','BE5','BE6','BE7','BE8','BE9','BE+','FR-BE','FZ-BE','FR-BE+','FZ-BE+'] - - Ar_Phase = ['S-E','S-AR+','S-AR4P','SEB-E','SEB-AR+','SEB-AR4P','SRCE-2437','FR-E','FZ-E','TE','PPOT','POW-ALL'] - O2_Phase = ['S-E','S-O+','S-O-','S-O2+','SEB-O+','SEB-O-','SEB-O2+','S-O3P3P','SEB-O3P3P','TE','PPOT','FR-E','FZ-E'] - - PRCCPAr_PCMC = ['AR^0.35','EB-0.35','ION-TOT0.35'] - PRCCPO2_PCMC = ['O^0.35','EB-0.35','ION-TOT0.35'] - GECCCP2a_BE_PCMC = ['AR^ 2.6 L','BE^ 2.6 L','EB- 2.6 L'] - - #################### - #====================================================================# #SWITCHBOARD AND DIAGNOSTICS# #====================================================================# # Requested IEDF/NEDF Variables. - IEDFVariables = GECCCP2a_BE_PCMC # Requested Variables from iprofile_2d.pdt - NEDFVariables = [] # Requested Variables from nprofile_2d.pdt + IEDFVariables = config.chemistry.IEDF_variables # Requested Variables from iprofile_2d.pdt + NEDFVariables = config.chemistry.NEDF_variables # Requested Variables from nprofile_2d.pdt # Requested movie1/movie_icp Variables. - PhaseVariables = Ar_Phase # Requested Movie1 (phase) Variables. - electrodeloc = [0,0] # Cell location of powered electrode [R,Z]. - waveformlocs = [] # Cell locations of additional waveforms [R,Z]. + PhaseVariables = config.chemistry.phase_variables # Requested Movie1 (phase) Variables. + electrodeloc = config.chemistry.electrode_loc # Cell location of powered electrode [R,Z]. + waveformlocs = config.chemistry.waveform_locs # Cell locations of additional waveforms [R,Z]. # Requested variables and plotting locations. - Variables = Phys+Ar+O2+Be+Al # Requested Variables from Tecplot2D.pdt, tecplot_kin.pdt, and movie_icp.pdt - multivar = [] # Additional variables plotted ontop of [Variables] - radialprofiles = [] # Radial 1D-Profiles to be plotted (fixed Z-mesh) -- - axialprofiles = [0] # Axial 1D-Profiles to be plotted (fixed R-mesh) | - probeloc = [] # Cell location For Trend Analysis [R,Z], (leave empty for global min/max) + Variables = config.chemistry.variables # Requested Variables from Tecplot2D.pdt, tecplot_kin.pdt, and movie_icp.pdt + multivar = config.chemistry.multivar # Additional variables plotted ontop of [Variables] + radialprofiles = config.chemistry.radial_profiles # Radial 1D-Profiles to be plotted (fixed Z-mesh) -- + axialprofiles = config.chemistry.axial_profiles # Axial 1D-Profiles to be plotted (fixed R-mesh) | + probeloc = config.chemistry.probe_loc # Cell location For Trend Analysis [R,Z], (leave empty for global min/max) # Various Diagnostic Settings >>> OUTDATED, TO BE RETIRED <<< - sheathROI = [] # Sheath Region of Interest, (Start,End) [cells] - sourcewidth = [] # Source Dimension at ROI, leave empty for auto. [cells] + sheathROI = config.chemistry.sheath_ROI # Sheath Region of Interest, (Start,End) [cells] + sourcewidth = config.chemistry.source_width # Source Dimension at ROI, leave empty for auto. [cells] # Requested diagnostics and plotting routines. - savefig_tecplot2D = True # 2D Single-Variables: TECPLOT2D.PDT < .csv File Save + savefig_tecplot2D = config.figures.tecplot2D # 2D Single-Variables: TECPLOT2D.PDT < .csv File Save - savefig_movieicp2D = False # 2D Variables against space-axis: movie_icp.pdt < MAXITER SHOULD BE AN ARRAY - savefig_movieicp1D = False # 1D Variables against space-axis: movie_icp.pdt < MAXITER SHOULD BE AN ARRAY - savefig_timeaxis1D = False # 1D Variables against time-axis: movie_icp.pdt - savefig_convergence = False # 1D variables against ITER-axis: movie_icp.pdt - iterstep = 1 # movie_icp.pdt iteration step size + savefig_movieicp2D = config.figures.movieicp2D # 2D Variables against space-axis: movie_icp.pdt < MAXITER SHOULD BE AN ARRAY + savefig_movieicp1D = config.figures.movieicp1D # 1D Variables against space-axis: movie_icp.pdt < MAXITER SHOULD BE AN ARRAY + savefig_timeaxis1D = config.figures.timeaxis1D # 1D Variables against time-axis: movie_icp.pdt + savefig_convergence = config.figures.convergence # 1D variables against ITER-axis: movie_icp.pdt + iterstep = config.figures.iterstep # movie_icp.pdt iteration step size - savefig_monoprofiles = False # 1D Variables against space-axis: TECPLOT2D < .csv File Save - savefig_multiprofiles = False # 1D Variables Compared Same Sims: TECPLOT2D - savefig_compareprofiles = False # 1D Variables Compared Between Sims: TECPLOT2D + savefig_monoprofiles = config.figures.monoprofiles # 1D Variables against space-axis: TECPLOT2D < .csv File Save + savefig_multiprofiles = config.figures.multiprofiles # 1D Variables Compared Same Sims: TECPLOT2D + savefig_compareprofiles = config.figures.compare_profiles # 1D Variables Compared Between Sims: TECPLOT2D # ^^^^ # NOTE: IMAGEPLOTTER1D RETURNS ARRAY ORDERED AS [0,height] <<< REVERSED RELATIVE TO HPEM # IMAGEPLOTTER2D RETURNS ARRAY ORDERED AS [height,0] <<< SAME ORIENTATION AS HPEM # 1D PROFILES ARE MANUALLY REVERSED ([::-1]) IN THE 1D DIAGNOSTICS TO ACCOUNT FOR THIS # IT WOULD BE BETTER TO USE "DataExtent" TO PROVIDE THE CORRECT ORIENTATION - savefig_trendphaseaveraged = False # Phase averaged trends at axial/radial cells # CHANGE TO 'ProbeLoc' cell - savefig_trendphaseresolved = False # Phase resolved trends at axial/radial cells # CHANGE TO 'ProbeLoc' cell - thrustloc = 45 # Z-axis cell for thrust calculation [Cells] + savefig_trendphaseaveraged = config.figures.trend_phase_averaged # Phase averaged trends at axial/radial cells # CHANGE TO 'ProbeLoc' cell + savefig_trendphaseresolved = config.figures.trend_phase_resolved # Phase resolved trends at axial/radial cells # CHANGE TO 'ProbeLoc' cell + thrustloc = config.figures.thrust_loc # Z-axis cell for thrust calculation [Cells] - savefig_phaseresolve2D = False # 2D Phase Resolved Images < .csv File Save - savefig_phaseresolve1D = False # 1D Phase Resolved Images < .csv File Save - savefig_sheathdynamics = False # 1D and 2D sheath dynamics images - savefig_PROES = False # Simulated PROES Diagnostic - phasecycles = 1.01 # Vaveform phase cycles to be plotted. [Float] - DoFwidth = 0 # PROES Depth of Field (symmetric about image plane) [Cells] + savefig_phaseresolve2D = config.figures.phase_resolve2D # 2D Phase Resolved Images < .csv File Save + savefig_phaseresolve1D = config.figures.phase_resolve1D # 1D Phase Resolved Images < .csv File Save + savefig_sheathdynamics = config.figures.sheath_dynamics # 1D and 2D sheath dynamics images + savefig_PROES = config.figures.PROES # Simulated PROES Diagnostic + phasecycles = config.figures.phase_cycles # Vaveform phase cycles to be plotted. [Float] + DoFwidth = config.figures.do_Fwidth # PROES Depth of Field (symmetric about image plane) [Cells] - savefig_IEDFangular = False # 2D images of angular IEDF; single folders < .csv File Save - savefig_IEDFtrends = False # 1D IEDF trends; all folders - savefig_EEDF = False # 1D EEDF trends; all folders < No Routine + savefig_IEDFangular = config.figures.IEDF_angular # 2D images of angular IEDF; single folders < .csv File Save + savefig_IEDFtrends = config.figures.IEDF_trends # 1D IEDF trends; all folders + savefig_EEDF = config.figures.EEDF # 1D EEDF trends; all folders < No Routine # Write processed data to ASCII files. - write_ASCII = False # Data underpinning figs written in ASCII format < Outdated - Write_CSV = True # Data underpinning figs written in .csv format + write_ASCII = config.write.ASCII # Data underpinning figs written in ASCII format < Outdated + Write_CSV = config.write.CSV # Data underpinning figs written in .csv format # ^^^^ # NOTE: SHEATH EXTENT SAVES WITH WRONG NAME, IN WRONG FILE FORMAT, IN ROOT DIRECTORY # Steady-State diagnostics terminal output toggles. - print_generaltrends = False # Verbose Min/Max Trend Outputs. - print_Knudsennumber = False # Print cell averaged Knudsen Number - print_totalpower = False # Print all requested total powers - print_Reynolds = False # Print cell averaged sound speed - print_DCbias = False # Print DC bias at electrodeloc - print_thrust = False # Print neutral, ion and total thrust - print_sheath = False # Print sheath width at electrodeloc + print_generaltrends = config.printout.general_trends # Verbose Min/Max Trend Outputs. + print_Knudsennumber = config.printout.Knudsen_number # Print cell averaged Knudsen Number + print_totalpower = config.printout.total_power # Print all requested total powers + print_Reynolds = config.printout.Reynolds # Print cell averaged sound speed + print_DCbias = config.printout.DC_bias # Print DC bias at electrodeloc + print_thrust = config.printout.thrust # Print neutral, ion and total thrust + print_sheath = config.printout.sheath # Print sheath width at electrodeloc # Image plotting options. - image_extension = '.png' # Define image extension ('.png', '.jpg', '.eps') - image_interp = 'spline36' # Define image smoothing ('none', 'bilinear','quadric','spline36') - image_cmap = 'plasma' # Define global colourmap ('jet','plasma','inferno','gnuplot','tecmodern') + image_extension = config.image.extension # Define image extension ('.png', '.jpg', '.eps') + image_interp = config.image.interp # Define image smoothing ('none', 'bilinear','quadric','spline36') + image_cmap = config.image.cmap # Define global colourmap ('jet','plasma','inferno','gnuplot','tecmodern') - image_aspectratio = [10,10] # Real Size of [X,Y] in cm [Doesn't Rotate - X is always horizontal] - image_radialcrop = [] # Crops 2D images to [R1,R2] in cm - image_axialcrop = [] # Crops 2D images to [Z1,Z2] in cm + image_aspectratio = config.image.aspect_ratio # Real Size of [X,Y] in cm [Doesn't Rotate - X is always horizontal] + image_radialcrop = config.image.radial_crop # Crops 2D images to [R1,R2] in cm + image_axialcrop = config.image.axial_crop # Crops 2D images to [Z1,Z2] in cm - image_plotsymmetry = False # Plot radial symmetry - mirrors across the ISYM axis - image_plotmesh = True # Plot material mesh outlines - image_plotgrid = False # Plot major/minor gridlines on 1D profiles - image_rotate = False # Rotate 2D images 90 degrees to the right. + image_plotsymmetry = config.image.plot_symmetry # Plot radial symmetry - mirrors across the ISYM axis + image_plotmesh = config.image.plot_mesh # Plot material mesh outlines + image_plotgrid = config.image.plot_grid # Plot major/minor gridlines on 1D profiles + image_rotate = config.image.rotate # Rotate 2D images 90 degrees to the right. - image_plotcolourfill = True # Plot 2D image colour fill - image_plotcontours = True # Plot 2D image contour lines - image_contourlvls = 10 # Number of contour levels + image_plotcolourfill = config.image.plot_colourfill # Plot 2D image colour fill + image_plotcontours = config.image.plot_contours # Plot 2D image contour lines + image_contourlvls = config.image.contour_lvls # Number of contour levels - image_axisticks = True # Toggle to show axis ticks and associated values or not - image_axislabels = True # Toggle to show axis labels or not - image_legendloc = 'best' # Set Legend Location, "1-9" or 'best' for automatic - image_cbarticks = True # Toggle to show cbar ticks and associated values or not - image_cbarbins = 5 # Set number of colourbar bins - image_cbarlimit = [] # Set arbitrary [min,max] colourbar limits + image_axisticks = config.image.axis_ticks # Toggle to show axis ticks and associated values or not + image_axislabels = config.image.axis_labels # Toggle to show axis labels or not + image_legendloc = config.image.legend_loc # Set Legend Location, "1-9" or 'best' for automatic + image_cbarticks = config.image.cbar_ticks # Toggle to show cbar ticks and associated values or not + image_cbarbins = config.image.cbar_bins # Set number of colourbar bins + image_cbarlimit = config.image.cbar_limit # Set arbitrary [min,max] colourbar limits - image_plotvector = True # Plot vector arrows onto 2D images (uses FR-XX, FZ-XX if they exist) - image_vectordensity = 1.5 # Vector line density, higher means more dense streamlines - image_vectorlw = 1.0 # Vector line width, higher means thicker streamlines + image_plotvector = config.image.plot_vector # Plot vector arrows onto 2D images (uses FR-XX, FZ-XX if they exist) + image_vectordensity = config.image.vector_density # Vector line density, higher means more dense streamlines + image_vectorlw = config.image.vector_lw # Vector line width, higher means thicker streamlines - image_normalise = False # Plot Data normlised to maximum value (Applies to all outputs) - image_logplot = False # Plot log10(Data) (Applies to all outputs) + image_normalise = config.image.normalise # Plot Data normlised to maximum value (Applies to all outputs) + image_logplot = config.image.log_plot # Plot log10(Data) (Applies to all outputs) - image_plotsheath = False # Plot sheath extent onto 2D images 'Axial','Radial' - image_plotphasewaveform = False # Plot waveform sub-figure on phaseresolve2D images - image_plotoverlay = False # Plot location(s) of 1D radial/axial profiles onto 2D images + image_plotsheath = config.image.plot_sheath # Plot sheath extent onto 2D images 'Axial','Radial' + image_plotphasewaveform = config.image.plot_phase_waveform # Plot waveform sub-figure on phaseresolve2D images + image_plotoverlay = config.image.plot_overlay # Plot location(s) of 1D radial/axial profiles onto 2D images # Image Overrides (Applies to all images) - titleoverride = [] - legendoverride = [] - xaxisoverride = [] - xlabeloverride = [] - ylabeloverride = [] + titleoverride = config.overrides.title + legendoverride = config.overrides.legend + xaxisoverride = config.overrides.xaxis + xlabeloverride = config.overrides.xlabel + ylabeloverride = config.overrides.ylabel #============================# @@ -6595,17 +6843,17 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): #for all variables requested by the user. for i in tqdm(range(0,len(VariableIndices))): - + #Append new list to convergenceTrends for each variable. ConvergenceTrends.append(list()) - + #for each iteration for k in range(0,len(MovieIterlist[l]),iterstep): #Extract full 2D image for further processing. Image = ImageExtractor2D(IterMovieData[l][k][VariableIndices[i]],VariableStrings[i]) - #If probeloc is provided, take convergence trend from that location + #If probeloc is provided, take convergence trend from that location #Else use the MESH AVERAGED VALUE for general convergence trend if len(probeloc) == 2: ConvergenceVal = Image[probeloc[0],probeloc[1]] @@ -6614,11 +6862,11 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): ConvergenceVal = sum(Image.flatten())/len(Image.flatten()) labelstring = 'Normalised Mesh-Averaged Values' #endif - + ConvergenceTrends[i] = np.append(ConvergenceTrends[i], ConvergenceVal) #endfor #endfor - + # Exit if less than two trend variables have been extracted Continue = True if len(ConvergenceTrends[0]) < 2: @@ -6631,7 +6879,7 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): #endif #=================# - + if Continue == True: #Plot a convergence check for all variables in each folder. @@ -6644,9 +6892,9 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): ConvergenceTrends[i] = Normalise(ConvergenceTrends[i],NormFactor)[0] ax.plot(IterAxis,ConvergenceTrends[i], lw=2) #endfor - + #Various debug outputs - if IDEBUG == True: + if IDEBUG == True: print(' ') print('Convergence Limits:',Limits) print(' ') @@ -6657,25 +6905,25 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): Xlabel,Ylabel = 'Simulation Iteration [-]',labelstring ImageOptions(fig,ax,Xlabel,Ylabel,Title,Legend,Crop=False) plt.tight_layout() - + # Compute and display convergence trends to terminal print('') print('Percentage Variation At Final Iteration:') print('Var \t Relative % \t Abs Delta \t Abs Sum') for i in range(0,len(ConvergenceTrends)): - + # The mesh averaged value # NOTE, NORMALISED TO MAX VALUE FOR VARIABLE [i] # MeshAveragedVal = abs(ConvergenceTrends[i][-1]) - + # Compute absolute delta and absolute sum AbsoluteDelta = abs(ConvergenceTrends[i][-1] - ConvergenceTrends[i][-2]) AbsoluteSum = abs(ConvergenceTrends[i][-1]) + abs(ConvergenceTrends[i][-2]) - + # Compute symmetric relative change (2*deltaValue / (Value[n] + Value[n-1])) RelativeChange = (2*AbsoluteDelta / AbsoluteSum ) RelativePercent = round(RelativeChange*100.0, 4) - + # Print convergence data to terminal print( VariableStrings[i], '\t', @@ -6683,7 +6931,7 @@ def PlotSheathExtent(SxAxis,Sx,ax=plt.gca(),ISymmetry=0,Orientation='Radial'): 'Δ=', round(AbsoluteDelta, 5), '\t', 'Σ=', round(AbsoluteSum, 5) ) - #endfor + #endfor #Save figure. savefig(DirConvergence+FolderNameTrimmer(Dirlist[l])+'_Convergence'+ext) diff --git a/tests/__snapshots__/test_regression.test_AR2plus.json b/tests/__snapshots__/test_input.py::test_AR2plus[input_tecplot2D.toml].json similarity index 100% rename from tests/__snapshots__/test_regression.test_AR2plus.json rename to tests/__snapshots__/test_input.py::test_AR2plus[input_tecplot2D.toml].json diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..22b30f0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +import os +import shutil +import pathlib +import pytest + + +@pytest.fixture(scope="session") +def input_directory(): + return pathlib.Path(__file__).parent / "input_files" + + +@pytest.fixture(scope="session") +def data_directory(): + return pathlib.Path(__file__).parent / "data" + + +@pytest.fixture(scope="session") +def test_env(tmp_path_factory, data_directory): + """ + Copy the expensive/shared input data once per test session. + """ + root = tmp_path_factory.mktemp("helena_env") + shutil.copytree(data_directory, root / "data") + return root + + +@pytest.fixture +def work_env(tmp_path, test_env): + """ + Give each test its own isolated copy of the seeded environment. + """ + workdir = tmp_path / "work" + shutil.copytree(test_env / "data", workdir / "data") + return workdir + + +@pytest.fixture +def run_in_tmp_dir(work_env, monkeypatch): + """ + Temporarily change the directory to the working directory. + """ + monkeypatch.chdir(work_env) diff --git a/tests/input_files/input_tecplot2D.toml b/tests/input_files/input_tecplot2D.toml new file mode 100644 index 0000000..68f101a --- /dev/null +++ b/tests/input_files/input_tecplot2D.toml @@ -0,0 +1,151 @@ +[chemistry] +variables = ["TEST"] +multivar = [] +radial_profiles = [] +axial_profiles = [0] +probe_loc = [] + +[chemistry.EDF] +IEDF_variables = ["GECCCP2a_BE_PCMC"] +NEDF_variables = [] + +[chemistry.movie] +phase_variables = ["Ar_Phase"] +electrode_loc = [0,0] +waveform_locs = [] + +[chemistry.diagnostic] +sheath_ROI = [] +source_width = [] + +[figures] +tecplot2D = true + +[figures.movie] +movieicp2D = false +movieicp1D = false +timeaxis1D = false +convergence = false +iterstep = 1 + +[figures.profiles] +monoprofiles = false +multiprofiles = false +compare_profiles = false + +[figures.trends] +trend_phase_averaged = false +trend_phase_resolved = false +thrust_loc = 45 + +[figures.resolve] +phase_resolve2D = false +phase_resolve1D = false +sheath_dynamics = false +PROES = false +phase_cycles = 1.01 +do_Fwidth = 0 + +[figures.EDF] +IEDF_angular = false +IEDF_trends = false +EEDF = false + +[write] +ASCII = false +CSV = true + +[print] +general_trends = false +Knudsen_number = false +total_power = false +Reynolds = false +DC_bias = false +thrust = false +sheath = false + +[image] +extension = '.png' + +[image.style] +interp = 'spline36' +cmap = 'plasma' + +[image.geometry] +aspect_ratio = [10,10] +radial_crop = [] +axial_crop = [] +rotate = false + +[image.display] +plot_symmetry = false +plot_mesh = true +plot_grid = false + +[image.contours] +plot_colourfill = true +plot_contours = true +contour_lvls = 10 + +[image.axes] +axis_ticks = true +axis_labels = true +legend_loc = 'best' + +[image.colorbar] +cbar_ticks = true +cbar_bins = 5 +cbar_limit = [] + +[image.vector] +plot_vector = true +vector_density = 1.5 +vector_lw = 1.0 + +[image.processing] +normalise = false +log_plot = false + +[image.overlays] +plot_sheath = false +plot_phase_waveform = false +plot_overlay = false + +[overrides] +title = [] +legend = [] +xaxis = [] +xlabel = [] +ylabel = [] + +#=======================================================# + +[expert.debug] +magmesh = 1 +ffmpeg_movies = false +IDEBUG = false + +[expert.warnings] +ignore_div_zero = true +ignore_empty_contours = true + +[expert.numerical] +sheath_method = 'AbsDensity' +thrust_method = 'AxialMomentum' +mean_calculation = 'MeanFraction' + +[expert.overrides] +DC_bias_axis = 'Auto' +sheath_ion_species = ['AR+'] + +[expert.filtering] +kinetic_filtering = true +plot_kinetic_filtering = false +sav_window = 25 +sav_poly_order = 3 + +[expert.misc] +sheath_ion_ratio_threshold = 1.03 +conv_azimuthal_phase = true +EDF_threshold = 0.01 +units = 'SI' \ No newline at end of file diff --git a/tests/input_files/input_wrong_type.toml b/tests/input_files/input_wrong_type.toml new file mode 100644 index 0000000..6080216 --- /dev/null +++ b/tests/input_files/input_wrong_type.toml @@ -0,0 +1,2 @@ +[chemistry] +variables = [3] \ No newline at end of file diff --git a/tests/test_input.py b/tests/test_input.py new file mode 100644 index 0000000..00167a7 --- /dev/null +++ b/tests/test_input.py @@ -0,0 +1,26 @@ +import pytest +from pydantic import ValidationError + +from helena import run + + +@pytest.mark.usefixtures("run_in_tmp_dir") +@pytest.mark.parametrize( + "input_file", + ["input_tecplot2D.toml"], +) +def test_AR2plus(input_file, input_directory, work_env, snaptolshot): + run(argv=[str(input_directory / input_file)]) + + output_file = work_env / "data/TECPlot2D/2DPlots_Data/AR2+.csv" + assert output_file.exists() + + output_lines = output_file.read_text().splitlines(keepends=True) + assert output_lines + assert snaptolshot == output_lines + + +@pytest.mark.usefixtures("run_in_tmp_dir") +def test_input(input_directory): + with pytest.raises(ValidationError): + run(argv=[str(input_directory / "input_wrong_type.toml")]) diff --git a/tests/test_regression.py b/tests/test_regression.py deleted file mode 100644 index 66b9857..0000000 --- a/tests/test_regression.py +++ /dev/null @@ -1,32 +0,0 @@ -import pathlib - -from helena import run - - -def test_AR2plus(snaptolshot): - import os - - os.chdir("tests/") - - output_file = pathlib.Path("data/TECPlot2D/2DPlots_Data/AR2+.csv") - - output_file.unlink(missing_ok=True) - - run(argv=[]) - - assert output_file.exists() - - with output_file.open("r") as f: - output_lines = f.readlines() - - assert len(output_lines) > 0 - - assert snaptolshot == output_lines - - pathlib.Path("data/meshnodes.dat").unlink() - pathlib.Path("data/TECPlot2D/2DPlots_Data/AR2+.csv").unlink() - pathlib.Path("data/TECPlot2D/2DPlot_AR2+.png").unlink() - pathlib.Path("data/TECPlot2D/2DPlots_Data").rmdir() - pathlib.Path("data/TECPlot2D").rmdir() - - os.chdir("../") diff --git a/uv.lock b/uv.lock index f541658..50f2545 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 2 requires-python = ">=3.11" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -157,7 +166,9 @@ source = { editable = "." } dependencies = [ { name = "matplotlib" }, { name = "numpy" }, + { name = "pydantic" }, { name = "scipy" }, + { name = "toml" }, { name = "tqdm" }, ] @@ -174,10 +185,12 @@ tests = [ requires-dist = [ { name = "matplotlib", specifier = ">=3.10.8" }, { name = "numpy", specifier = ">=2.4.2" }, + { name = "pydantic", specifier = ">=2.12.5" }, { name = "pytest", marker = "extra == 'tests'", specifier = ">=9.0.2" }, { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.15.4" }, { name = "scipy", specifier = ">=1.17.1" }, - { name = "snaptol", marker = "extra == 'tests'", specifier = ">=0.0.3" }, + { name = "snaptol", marker = "extra == 'tests'", specifier = ">=0.0.4" }, + { name = "toml", specifier = ">=0.10.2" }, { name = "tqdm", specifier = ">=4.67.3" }, ] provides-extras = ["tests", "lint"] @@ -529,6 +542,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -682,15 +807,24 @@ wheels = [ [[package]] name = "snaptol" -version = "0.0.3" +version = "0.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/36/d78a0002c921b0297b72ac35d852d6892790563d4b6a696d59d876cb69bb/snaptol-0.0.3.tar.gz", hash = "sha256:6892e6bfc6108c8d94a4c1242a82b96751c3ce9c639bf3ec9e821d72ad9a0919", size = 102884, upload-time = "2026-02-17T10:27:47.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/f3/745fe4e9248cf7372bc3ced73d52e83185e60ffc0f3193520e69198ae0cf/snaptol-0.0.4.tar.gz", hash = "sha256:62b7759add1aab7cd8abb3a2a25ff824259ac837fab851568c01df1dd12577be", size = 102905, upload-time = "2026-03-26T19:22:29.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/99/c52a1cabc65247a419ca6dcb9114a488c5fc3f5d1a176430d053bf055271/snaptol-0.0.3-py3-none-any.whl", hash = "sha256:af5a191209f934e4e1f65079dd601aec9aa85e5283c832ad67c27bfc00b353d9", size = 13013, upload-time = "2026-02-17T10:27:45.563Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/3c5cb4cf7e7a8902c66dfd4b775528b813fa03bce959aedd1ab63ac03ccb/snaptol-0.0.4-py3-none-any.whl", hash = "sha256:f22a1a6f982c9d9030a36652157c966f89928c95789ef89b35fc238e7e755cff", size = 13024, upload-time = "2026-03-26T19:22:28.018Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] @@ -704,3 +838,24 @@ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +]