From c6bdab85389a0a362472e729ca157e1fde530658 Mon Sep 17 00:00:00 2001 From: Cameron Flannery Date: Tue, 25 Nov 2025 23:11:44 -0800 Subject: [PATCH 1/3] bring this ancient project into the modern era --- Dockerfile | 8 - License | 2 +- build_env.sh | 38 - docs/conf.py | 1 - openrocketengine/__init__.py | 83 ++ openrocketengine/core/interface.py | 80 -- openrocketengine/core/rocket.py | 649 --------------- openrocketengine/core/test_config.cfg | 16 - openrocketengine/core/writer.py | 47 -- openrocketengine/engine.py | 528 +++++++++++++ openrocketengine/examples/__init__.py | 2 + openrocketengine/examples/basic_engine.py | 225 ++++++ openrocketengine/isentropic.py | 633 +++++++++++++++ openrocketengine/nozzle.py | 425 ++++++++++ openrocketengine/plotting.py | 659 ++++++++++++++++ openrocketengine/units.py | 622 +++++++++++++++ pyproject.toml | 60 ++ requirements.txt | 5 - setup.py | 13 - tests/test_core.py | 5 - tests/test_engine.py | 412 ++++++++++ tests/test_isentropic.py | 411 ++++++++++ tests/test_nozzle.py | 368 +++++++++ tests/test_units.py | 402 ++++++++++ uv.lock | 916 ++++++++++++++++++++++ 25 files changed, 5747 insertions(+), 863 deletions(-) delete mode 100644 Dockerfile delete mode 100644 build_env.sh create mode 100644 openrocketengine/__init__.py delete mode 100644 openrocketengine/core/interface.py delete mode 100644 openrocketengine/core/rocket.py delete mode 100644 openrocketengine/core/test_config.cfg delete mode 100644 openrocketengine/core/writer.py create mode 100644 openrocketengine/engine.py create mode 100644 openrocketengine/examples/__init__.py create mode 100644 openrocketengine/examples/basic_engine.py create mode 100644 openrocketengine/isentropic.py create mode 100644 openrocketengine/nozzle.py create mode 100644 openrocketengine/plotting.py create mode 100644 openrocketengine/units.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 tests/test_core.py create mode 100644 tests/test_engine.py create mode 100644 tests/test_isentropic.py create mode 100644 tests/test_nozzle.py create mode 100644 tests/test_units.py create mode 100644 uv.lock diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d653898..0000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:3.7-stretch -RUN mkdir /code -WORKDIR /code -COPY . /code - -RUN pip install -r requirements.txt - -RUN pytest diff --git a/License b/License index 621fc0e..5a193d4 100644 --- a/License +++ b/License @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Cameron Flannery +Copyright (c) 2025 Cameron Flannery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build_env.sh b/build_env.sh deleted file mode 100644 index 255b5a2..0000000 --- a/build_env.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# -# This is a simple script that performs automates some simple things required -# for developing design_optimization -echo 'Running build_env.sh' -echo '' - - -# Create a python 3 virtual environment -# If a virtual environment already exists, delete it. -if [ -d './venv' ] -then - echo 'A virtual environment already exists. Deleting and starting from scratch.' - rm -rf venv - rm -rf $(find ./ -name '*.egg-info') -fi -echo 'Creating virtual environment.' -echo '' -python3 -m venv venv - -source venv/bin/activate -pip install wheel - -# Install requirements -echo 'Installing development requirements' -if [ -f 'requirements-dev.txt' ] -then - pip install -r requirements-dev.txt -fi - -echo 'Installing package requirements' -if [ -f 'requirements.txt' ] -then - pip install -r requirements.txt -fi - -echo 'Installing package and dependencies' -pip install -e . diff --git a/docs/conf.py b/docs/conf.py index 7fe106c..ada59d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # diff --git a/openrocketengine/__init__.py b/openrocketengine/__init__.py new file mode 100644 index 0000000..cb1a4b5 --- /dev/null +++ b/openrocketengine/__init__.py @@ -0,0 +1,83 @@ +"""OpenRocketEngine - Tools for liquid rocket engine design and analysis. + +This package provides a comprehensive toolkit for designing and analyzing +liquid propellant rocket engines using isentropic flow equations. + +Example: + >>> from openrocketengine import EngineInputs, design_engine + >>> from openrocketengine.units import newtons, megapascals, kelvin, meters, pascals + >>> + >>> inputs = EngineInputs( + ... thrust=newtons(5000), + ... chamber_pressure=megapascals(2.0), + ... chamber_temp=kelvin(3200), + ... exit_pressure=pascals(101325), + ... molecular_weight=22.0, + ... gamma=1.2, + ... lstar=meters(1.0), + ... mixture_ratio=2.0, + ... ) + >>> performance, geometry = design_engine(inputs) + >>> print(f"Isp: {performance.isp.value:.1f} s") +""" + +__version__ = "0.2.0" + +# Core engine design +from openrocketengine.engine import ( + EngineGeometry, + EngineInputs, + EnginePerformance, + compute_geometry, + compute_performance, + design_engine, + format_geometry_summary, + format_performance_summary, + isp_at_altitude, + thrust_at_altitude, +) + +# Nozzle contour generation +from openrocketengine.nozzle import ( + NozzleContour, + conical_contour, + full_chamber_contour, + generate_nozzle_from_geometry, + rao_bell_contour, +) + +# Visualization +from openrocketengine.plotting import ( + plot_engine_cross_section, + plot_engine_dashboard, + plot_nozzle_contour, + plot_performance_vs_altitude, +) + +__all__ = [ + # Version + "__version__", + # Engine dataclasses + "EngineInputs", + "EnginePerformance", + "EngineGeometry", + # Engine computation + "compute_performance", + "compute_geometry", + "design_engine", + "thrust_at_altitude", + "isp_at_altitude", + "format_performance_summary", + "format_geometry_summary", + # Nozzle + "NozzleContour", + "rao_bell_contour", + "conical_contour", + "full_chamber_contour", + "generate_nozzle_from_geometry", + # Plotting + "plot_engine_cross_section", + "plot_nozzle_contour", + "plot_performance_vs_altitude", + "plot_engine_dashboard", +] diff --git a/openrocketengine/core/interface.py b/openrocketengine/core/interface.py deleted file mode 100644 index 0e759ec..0000000 --- a/openrocketengine/core/interface.py +++ /dev/null @@ -1,80 +0,0 @@ -"""User interface for openrocketengine. Reads config files and outputs data""" -from __future__ import print_function, division, absolute_import -import os -import pandas as pd -import fire -from openrocketengine.core.rocket import Engine - - -standard_types = { - "name": str, - "units": str, - "thrust": float, - "Tc": float, - "pc": float, - "pe": float, - "MR": float, - "MW": float, - "gamma": float, - "lstar": float, - "area_ratio": float, -} - - -def read_config(fname): - """Parse configuration file and return a dictionary with the key value pairs - - Parameters: - fname (str): file name, or path to file, of configuration file - - Returns: - dict: contains all parameters and values parsed and typecast - """ - assert os.path.isfile( - fname - ), "Error: {} does not exist in the working directory".format(fname) - with open(fname, "r") as f: - config = f.read() - - statements = [s for s in config.split("\n") if s != ""] # ignore empty lines - parsed_config = [s for s in statements if s[0] != "#"] # ignore all comment lines - configuration = {} - # Read the configuration file and store all key-value pairs in a dictionary - for thing in parsed_config: - param_value = thing.split(" ") - try: - parameter = param_value[0].rstrip().lstrip() - value = param_value[1].rstrip().lstrip() - configuration[parameter] = value - except IndexError: - pass - - # Parse through the data and convert types - for key in configuration.keys(): - try: - std_type = standard_types[key] - except KeyError: - raise Exception( - "Error: {} is not a known configuration parameter".format(key) - ) - - if std_type != type(configuration[key]): - configuration[key] = std_type(configuration[key]) - - return configuration - - -def main(fname): - configuration = read_config(fname) - engine = Engine(**configuration) - print(engine.ue) - engine.generate_output() - - -def entry(): - """Command line entry point""" - fire.Fire(main) - - -if __name__ == "__main__": - fire.Fire(main) diff --git a/openrocketengine/core/rocket.py b/openrocketengine/core/rocket.py deleted file mode 100644 index 9257d55..0000000 --- a/openrocketengine/core/rocket.py +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/env python -"""This module is part of Open Rocket Engine's engine development program. OpenRocketEngine -provides a command line interface for the design and development of rocket engine thrust -chambers.""" -from __future__ import division, absolute_import, print_function -import sys -import os -import subprocess -import numpy as np -import pandas as pd -from datetime import datetime -import xlsxwriter -from string import ascii_uppercase - -__author__ = "Cameron Flannery" -__copyright__ = "Copyright 2018" -__license__ = "MIT" - - -np.warnings.filterwarnings("ignore") - -SI_units = { - "thrust": "N", - "mass": "kg", - "isp": "s", - "mdot": "kg/s", - "unitless": "1", - "length": "m", - "area": "m^2", - "volume": "m^3", - "angle": "degrees", -} - -imperial_units = { - "thrust": "lbf", - "mass": "lbm", - "isp": "s", - "mdot": "lbm/s", - "unitless": "1", - "length": "ft", - "area": "ft^2", - "volume": "ft^3", - "angle": "degrees", -} -Units = dict(SI=SI_units, Imperial=imperial_units) - -# engine class retrieves and stores all outputs for each run -class Engine: - """Designs a rocket engine based on isentropic flow equations. - - Initialization requirements are listed in the parameters below. - - Parameters: - name (str): Common name for the engine - thrust (float): sea-level thrust - Tc (float): chamber temperature - pc (float): chamber pressure - pe (float): exit pressure - pa (float): ambient pressure - MR (float): mass ratio - MW (float): molecular weight - gamma (float): ratio of coefficients of heat - """ - - def __init__( - self, - thrust=None, - Tc=None, - pc=None, - pe=None, - pa=None, - MR=None, - MW=None, - gamma=None, - lstar=None, - area_ratio=None, - units=None, - contraction_angle=None, - bell_length=None, - name=None, - ): - self.units = units - self.name = name - - self.thrust = thrust - self.Tc = Tc - self.pc = pc - self.pe = pe - self.pa = pa - self.MR = MR - self.MW = MW - self.gamma = gamma - # geometric parameters - self.lstar = lstar - if area_ratio is not None: - self.contraction_area_ratio = area_ratio # chamber contraction area ratio - else: - self.contraction_area_ratio = 5 # nondimensional - - if contraction_angle is not None: - self.contraction_angle = contraction_angle # in degrees - else: - self.contraction_angle = 60 # degrees - - if bell_length is not None: - self.bell_length = bell_length - else: - self.bell_length = 0.8 - - if self.pa is None: - self.pa = self.pe - - # set constants - if self.units == "SI": - self.__Rbar = 8314 # [kJ/Kmol-K] - self.__g0 = 9.81 # m/s^2 - elif self.units == "Imperial": - self.__Rbar = 1545 # [ft-lbf/lbmol-R] - self.__g0 = 32.17 # ft/s^2 - - @property - def name(self): - """ name, engine name: optional parameter """ - return self.__name - - @name.setter - def name(self, value): - """ set name """ - self.__name = value - - ################################################################################ - # **************************** INDEPENDENT VARIABLES **************************** - ################################################################################ - @property - def Rbar(self): - """ Rbar, universal gas constant """ - return self.__Rbar - - @property - def g0(self): - """ g0, gravitational parameter """ - return self.__g0 - - @property - def thrust_vac(self): - """ Thrust in vacuum """ - self.__thrust_vac = self.thrust + self.pe * self.Ae - return self.__thrust_vac - - @property - def Tc(self): - """ Tc, chamber temperature property """ - return self.__Tc - - @Tc.setter - def Tc(self, value): - self.__Tc = value - - @property - def pc(self): - """ pc, chamber pressure property """ - return self.__pc - - @pc.setter - def pc(self, value): - self.__pc = value - - @property - def pa(self): - """ pa, ambient pressure property """ - return self.__pa - - @pa.setter - def pa(self, value): - self.__pa = value - - @property - def pe(self): - """ pe, exit pressure proprety """ - return self.__pe - - @pe.setter - def pe(self, value): - self.__pe = value - - @property - def MR(self): - """" MR, mixture ratio property """ - return self.__MR - - @MR.setter - def MR(self, value): - self.__MR = value - - @property - def MW(self): - """ MW, gas molecular weight of propellants """ - return self.__MW - - @MW.setter - def MW(self, value): - self.__MW = value - - @property - def gamma(self): - """ gamma, ratio of coefficients of heats """ - return self.__gamma - - @gamma.setter - def gamma(self, value): - self.__gamma = value - - ################################################################################ - # ***************************** DEPENDENT VARIABLES ***************************** - ################################################################################ - @property - def asound(self): - """ asound is the speed of sound """ - Rspecific = self.Rspecific - gamma = self.gamma - Tc = self.Tc - return np.sqrt(gamma * Rspecific * Tc) - - @property - def Rspecific(self): - """ Rspecific, specific gas constant """ - return self.Rbar / self.MW - - @Rspecific.setter - def Rspecific(self, MW): - self.__Rspecific = self.Rbar / MW - - @property - def cstar(self): - """ cstar, characteristic velocity - get_cstar() calculates and returns the value of cstar """ - gamma = self.gamma - Rspecific = self.Rspecific - Tc = self.Tc - return np.sqrt(gamma * Rspecific * Tc) / ( - gamma * np.sqrt((2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1))) - ) - - @cstar.setter - def cstar(self, value): - """ value, dictionary containing gamma, Rspecific, and Tc """ - g0 = self.g0 - required = ["gamma", "Rspecific", "Tc"] - - try: - gamma = value["gamma"] - Rspecific = value["Rspecific"] - Tc = value["Tc"] - except KeyError: - print("Include all required parameters:", required) - raise - - self.__cstar = np.sqrt(gamma * Rspecific * Tc) / ( - gamma * np.sqrt((2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1))) - ) - - @property - def Cf(self): - """ Cf, Thrust Coefficient """ - gamma = self.gamma - pc = self.pc - pe = self.pe - return np.sqrt( - (2 * gamma ** 2 / (gamma - 1)) - * (2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1)) - * (1 - (pe / pc) ** ((gamma - 1) / gamma)) - ) - - @Cf.setter - def Cf(self, value): - """ Cf, Thrust Coefficient """ - required = ["pe", "pc", "gamma"] - try: - pe = value["pe"] - pc = value["pc"] - gamma = value["gamma"] - except KeyError: - print("Include all required parameters:", required) - raise - - self.__Cf = np.sqrt( - (2 * gamma ** 2 / (gamma - 1)) - * (2 / (gamma + 1)) ** ((gamma + 1) / (gamma - 1)) - * (1 - (pe / pc) ** ((gamma - 1) / gamma)) - ) - - @property - def Isp(self): - """ Isp, Specific Impulse [s] - - Specific Impulse is a commonly used performance metric for rocket - engines - """ - cstar = self.cstar - Cf = self.Cf - g0 = self.g0 - return cstar * Cf / g0 - - @property - def Isp_vac(self): - """ Isp_vac, Specific Impulse in vacuum [s] - """ - thrust = self.thrust + (self.pe - 0) * self.Ae - return thrust / (self.mdot * self.g0) - - @property - def mdot(self): - """ mdot, total mass flow rate - - typically, - get_mdot(Isp=Isp, thrust=thrust, g0=g0)""" - thrust = self.thrust - Isp = self.Isp - g0 = self.g0 - return thrust / (Isp * g0) - - @property - def mdot_ox(self): - """ mdot_ox, mass flow rate of oxidizer""" - MR = self.MR - mdot = self.mdot - return MR / (MR + 1) * mdot - - @property - def mdot_f(self): - """ mdot_f, mass flow rate of fuel""" - MR = self.MR - mdot = self.mdot - return 1 / (MR + 1) * mdot - - @property - def Tt(self): - """ Calculate the throat temperature - - Derived from isentropic flow-critical temperature ratio """ - - return self.Tc / (1 + (self.gamma - 1) / 2) - - @property - def pt(self): - """ Calculate the throat pressure - - Derived from the isentropic flow-critical pressure ratio """ - pc = self.pc - gamma = self.gamma - return pc * (2 / (gamma + 1)) ** (gamma / (gamma - 1)) - - @property - def ue(self): - """ calculate the exhaust velocity """ - - gamma = self.gamma - Rspecific = self.Rspecific - Tc = self.Tc - pc = self.pc - pe = self.pe - self.__ue = np.sqrt( - 2 - * gamma - * Rspecific - / (gamma - 1) - * Tc - * (1 - (pe / pc) ** ((gamma - 1) / gamma)) - ) - return self.__ue - - @property - def Ma_exit(self): - """ Calculate and set the mach number at nozzle exit - - derived from pressure ratio equation""" - # ue = self.ue - # asound = self.asound - # self.__Ma_exit = ue/asound - # return self.__Ma_exit - gamma = self.gamma - pc = self.pc - pa = self.pa - return np.sqrt(2 / (gamma - 1) * ((pc / pa) ** ((gamma - 1) / gamma) - 1)) - - ################################################################################ - # **************************** Chamber Calculations ***************************** - ################################################################################ - - # Notes: - # Determine the area contraction ratio based on the injection velocities and - # compressible flow equations in a converging section. - # i.e. what convergence is necessary to achieve mach 1 in the throat? - # This is codable.. Add some margin for major losses - - @property - def Ac(self): - """ Ac returns the chamber area """ - return self.At * self.contraction_area_ratio - - @property - def Rc(self): - """ radius of combustion chamber """ - return np.sqrt(self.Ac / np.pi) - - @property - def Dc(self): - """ diameter of combustion chamber """ - return 2 * self.Rc - - @property - def At(self): - """ At returns the calculated area of the throat """ - mdot = self.mdot - cstar = self.cstar - pc = self.pc - return cstar * mdot / pc - - @property - def Rt(self): - """ radius of the throat """ - return np.sqrt(self.At / np.pi) - - @property - def Dt(self): - """ diameter of the throat """ - return self.Rt * 2 - - @property - def Ae(self): - """ Ae returns the exit area of the rocket, assuming ideal expansion - in a non-vacuum environment """ - return self.calc_A(self.Ma_exit) - - @property - def Re(self): - """ radius of exit """ - return np.sqrt(self.Ae / np.pi) - - @property - def De(self): - """ diameter of exit """ - return 2 * self.Re - - @property - def Rn(self): - """ radius of circular entrance region for parabolic approximation (rao) - i.e. region leaving the throat """ - return 0.382 * self.Rt # from Huzel and Huang, 76 - - @property - def R1(self): - """ radius of circular entrance region to throat """ - return 1.5 * self.Rt # from rao and Braeuing - - @property - def expansion_area_ratio(self): - """ returns the expansion area ratio, Aexit/Athroat """ - return self.Ae / self.At - - @property - def contraction_area_ratio(self): - """ The contraction area ratio is the value of Ac/Ae. A minimum value of 3 is recommended - to consistently achieve mach 1 in the throat """ - if self.__contraction_area_ratio: - return self.__contraction_area_ratio - else: - self.__contraction_area_ratio = self.__Ac / self.__Ae - if self.__contraction_area_ratio < 3: - print( - "Warning: A minimum area contraction ratio of 3 is recommended. \ - Use the Ae or contraction area ratio setter to change the value of Ac" - ) - return self.__contraction_area_ratio - - @contraction_area_ratio.setter - def contraction_area_ratio(self, value): - self.__contraction_area_ratio = value - - def calc_A(self, Ma): - """ calc_A returns the area at an arbitrary station relative to the - critical value, i.e. the throat in choked flow - - calc_A does not set any property of the class or class instance, only - a value is returned """ - - At = self.At - gamma = self.gamma - - return ( - At - / Ma - * ((1 + ((gamma - 1) / 2) * Ma ** 2) / ((1 + gamma) / 2)) - ** ((gamma + 1) / (2 * (gamma - 1))) - ) - - @property - def contraction_angle(self): - return self.__contraction_angle - - @contraction_angle.setter - def contraction_angle(self, value): - """ set in degrees """ - self.__contraction_angle = value - - @property - def lstar(self): - return self.__lstar - - @lstar.setter - def lstar(self, value): - """ expects value in inches """ - self.__lstar = value - - @property - def Vc(self): - """ combustion chamber volume """ - return self.lstar * self.At - - @property - def lcyl(self): - """ length of cylindrical section of combustion chamber """ - Vc = self.Vc - Ac = self.Ac - Rc = self.Rc - Rt = self.Rt - beta = self.contraction_angle # degrees - - return Vc / Ac - 1 / 2 * (Rc - Rt) / np.tan(np.deg2rad(beta)) - - @property - def bell_length(self): - """ the percentage length of a 15 degree conical nozzle - - default = 0.8 """ - - return self.__bell_length - - @bell_length.setter - def bell_length(self, value): - self.__bell_length = value - - @property - def ln(self): - """ length of nozzle """ - Re = self.Re - Rt = self.Rt - bell_length = self.bell_length - angle = 15.0 # degrees - return (Re - Rt) / np.tan(np.deg2rad(angle)) * bell_length - - # Misc Tasks - def generate_output(self): - """Creates output excel file with engine performance and geometric parameters""" - outputName = "rocket_{name}_{now}.xlsx".format( - name=self.name, now=datetime.utcnow().strftime("%Y_%m_%d") - ) - workbook = xlsxwriter.Workbook(outputName) - geometryWorksheet = workbook.add_worksheet("geometry") - performanceWorksheet = workbook.add_worksheet("performance") - - self._write_performance(performanceWorksheet) - self._write_geometry(geometryWorksheet) - - def _write_performance(self, performanceWorksheet): - """Write performance values to worksheet""" - performanceWorksheet.write("A1", "Engine Name:") - if self.name != False: - performanceWorksheet.write("B1", self.name) - - # generate header - performanceWorksheet.write("A3", "Thrust") - performanceWorksheet.write("A4", "Thrust Vac") - performanceWorksheet.write("A5", "Isp") - performanceWorksheet.write("A6", "Isp Vac") - performanceWorksheet.write("A7", "mass flow rate") - performanceWorksheet.write("A8", "Mixture Ratio") - - # add data - performanceWorksheet.write("B3", self.thrust) - performanceWorksheet.write("B4", self.thrust_vac) - performanceWorksheet.write("B5", self.Isp) - performanceWorksheet.write("B6", self.Isp_vac) - performanceWorksheet.write("B7", self.mdot) - performanceWorksheet.write("B8", self.MR) - - # units - performanceWorksheet.write("C3", Units[self.units]["thrust"]) - performanceWorksheet.write("C4", Units[self.units]["thrust"]) - performanceWorksheet.write("C5", Units[self.units]["isp"]) - performanceWorksheet.write("C6", Units[self.units]["isp"]) - performanceWorksheet.write("C7", Units[self.units]["mdot"]) - performanceWorksheet.write("C8", Units[self.units]["unitless"]) - - def _write_geometry(self, geometryWorksheet): - """Write geometry values to worksheet""" - geometryWorksheet.write("A1", "Engine Name:") - if self.name != False: - geometryWorksheet.write("B1", self.name) - - # generate header - geometryWorksheet.write("A3", "Ac, Chamber Area") - geometryWorksheet.write("A4", "Rc, Chamber Radius") - geometryWorksheet.write("A5", "At, Throat Area") - geometryWorksheet.write("A6", "Rt, Throat Radius") - geometryWorksheet.write("A7", "Ae, Exit Area") - geometryWorksheet.write("A8", "Re, Exit Radius") - geometryWorksheet.write("A9", "Rn, radius leaving thoat") - geometryWorksheet.write("A10", "Ea, expansion area ratio (Ae/At)") - geometryWorksheet.write("A11", "Ec, contraction area ratio (Ac/Ae)") - geometryWorksheet.write("A12", "Thetac, contraction angle") - geometryWorksheet.write("A13", "lstar") - geometryWorksheet.write("A14", "Vc, chamber volume") - geometryWorksheet.write( - "A15", "lcyl, cylindrical section of combustion chamber" - ) - geometryWorksheet.write("A16", "length of nozzle") - - # add data - geometryWorksheet.write("B3", self.Ac) - geometryWorksheet.write("B4", self.Rc) - geometryWorksheet.write("B5", self.At) - geometryWorksheet.write("B6", self.Rt) - geometryWorksheet.write("B7", self.Ae) - geometryWorksheet.write("B8", self.Re) - geometryWorksheet.write("B9", self.Rn) - geometryWorksheet.write("B10", self.expansion_area_ratio) - geometryWorksheet.write("B11", self.contraction_area_ratio) - geometryWorksheet.write("B12", self.contraction_angle) - geometryWorksheet.write("B13", self.lstar) - geometryWorksheet.write("B14", self.Vc) - geometryWorksheet.write("B15", self.lcyl) - geometryWorksheet.write("B16", self.ln) - - # units - geometryWorksheet.write("C3", Units[self.units]["area"]) - geometryWorksheet.write("C4", Units[self.units]["length"]) - geometryWorksheet.write("C5", Units[self.units]["area"]) - geometryWorksheet.write("C6", Units[self.units]["length"]) - geometryWorksheet.write("C7", Units[self.units]["area"]) - geometryWorksheet.write("C8", Units[self.units]["length"]) - geometryWorksheet.write("C9", Units[self.units]["length"]) - geometryWorksheet.write("C10", Units[self.units]["unitless"]) - geometryWorksheet.write("C11", Units[self.units]["unitless"]) - geometryWorksheet.write("C12", Units[self.units]["angle"]) - geometryWorksheet.write("C13", Units[self.units]["length"]) - geometryWorksheet.write("C14", Units[self.units]["volume"]) - geometryWorksheet.write("C15", Units[self.units]["length"]) - geometryWorksheet.write("C16", Units[self.units]["length"]) diff --git a/openrocketengine/core/test_config.cfg b/openrocketengine/core/test_config.cfg deleted file mode 100644 index d981ec2..0000000 --- a/openrocketengine/core/test_config.cfg +++ /dev/null @@ -1,16 +0,0 @@ -# This is a test configuration file for openrocketengine -# -# The parameters listed here are all the known parameters that openrocketengine can take as inputs. -# Refer to the official documentation for more implementation and usage details. -name RBF1 -units SI -thrust 5000 -Tc 3200 -pc 2068000 -pe 101325 -MR 2.1 -MW 18.9 -gamma 2.31 -# Geometric parameters -lstar 1.016 -area_ratio 5.5 diff --git a/openrocketengine/core/writer.py b/openrocketengine/core/writer.py deleted file mode 100644 index 676295f..0000000 --- a/openrocketengine/core/writer.py +++ /dev/null @@ -1,47 +0,0 @@ -import xlsxwriter -from string import ascii_uppercase - - -def generate(engine): - """ generate() - creates output excel file with engine performance and geometric parameters - """ - outputName = "engine.xlsx" - workbook = xlsxwriter.Workbook(outputName) - performanceWorksheet = workbook.add_worksheet("performance") - geometryWorksheet = workbook.add_worksheet("geometry") - - headersFormat = workbook.add_format() - headersFormat.set_bg_color(bg_color="#C7C7FF") - - performanceWorksheet.write("B2", "Engine Name:") - if engine.name != False: - performanceWorksheet.write("C2", engine.name) - - # generate header - performanceWorksheet.write("B3", "Thrust", headersFormat) - performanceWorksheet.write("B4", "Thrust Vac", headersFormat) - performanceWorksheet.write("B5", "Isp", headersFormat) - performanceWorksheet.write("B6", "Isp Vac", headersFormat) - performanceWorksheet.write("B7", "mass flow rate", headersFormat) - performanceWorksheet.write("B8", "Mixture Ratio", headersFormat) - - # add data - performanceWorksheet.write("C3", engine.thrust) - performanceWorksheet.write("C4", engine.thrust_vac) - performanceWorksheet.write("C5", engine.Isp) - performanceWorksheet.write("C6", engine.Isp_vac) - performanceWorksheet.write("C7", engine.mdot) - performanceWorksheet.write("C8", engine.MR) - performanceWorksheet.write("C9", engine.ue) - - # units - performanceWorksheet.write("D3", "N") - performanceWorksheet.write("D4", "N") - performanceWorksheet.write("D5", "s") - performanceWorksheet.write("D6", "s") - performanceWorksheet.write("D7", "kg/s") - performanceWorksheet.write("D8", "1") - performanceWorksheet.write("D9", "m/s") - - print("Output Generated!") diff --git a/openrocketengine/engine.py b/openrocketengine/engine.py new file mode 100644 index 0000000..197438d --- /dev/null +++ b/openrocketengine/engine.py @@ -0,0 +1,528 @@ +"""Engine module for OpenRocketEngine. + +This module provides the core data structures and computation functions for +rocket engine design and analysis. + +Design principles: +- Immutable dataclasses for all engine parameters +- Pure functions for all computations (no side effects) +- Explicit data flow: inputs -> performance -> geometry +- Type safety with beartype runtime checking +""" + +import math +from dataclasses import dataclass + +from beartype import beartype + +from openrocketengine.isentropic import ( + G0_SI, + area_ratio_from_mach, + bell_nozzle_length, + chamber_volume, + characteristic_velocity, + cylindrical_chamber_length, + diameter_from_area, + mach_from_pressure_ratio, + mass_flow_rate, + specific_gas_constant, + specific_impulse, + throat_area, + thrust_coefficient, + thrust_coefficient_vacuum, +) +from openrocketengine.units import ( + Quantity, + kg_per_second, + meters, + meters_per_second, + seconds, + square_meters, +) + +# ============================================================================= +# Input Data Structures +# ============================================================================= + + +@beartype +@dataclass(frozen=True, slots=True) +class EngineInputs: + """All inputs required to define a rocket engine. + + This immutable dataclass contains all the parameters needed to compute + engine performance and geometry. All physical quantities use the Quantity + class for type safety and unit handling. + + Attributes: + thrust: Sea-level thrust [force] + chamber_pressure: Chamber (stagnation) pressure [pressure] + chamber_temp: Chamber (stagnation) temperature [temperature] + exit_pressure: Nozzle exit pressure [pressure] + ambient_pressure: Ambient pressure for performance calculation [pressure] + molecular_weight: Molecular weight of exhaust gases [kg/kmol] + gamma: Ratio of specific heats (Cp/Cv) [-] + lstar: Characteristic chamber length [length] + mixture_ratio: Oxidizer to fuel mass ratio [-] + contraction_ratio: Chamber area / throat area [-] + contraction_angle: Chamber convergence half-angle [degrees] + bell_fraction: Bell nozzle length as fraction of 15° cone [-] + name: Optional engine name for identification + """ + + thrust: Quantity # Sea-level thrust + chamber_pressure: Quantity # pc + chamber_temp: Quantity # Tc + exit_pressure: Quantity # pe + molecular_weight: float # kg/kmol + gamma: float # Cp/Cv, dimensionless + lstar: Quantity # L*, characteristic length + mixture_ratio: float # O/F ratio, dimensionless + ambient_pressure: Quantity | None = None # pa, defaults to pe + contraction_ratio: float = 4.0 # Ac/At, dimensionless + contraction_angle: float = 45.0 # degrees + bell_fraction: float = 0.8 # fraction of 15° cone length + name: str | None = None + + def __post_init__(self) -> None: + """Validate inputs after initialization.""" + # Validate dimensions + if self.thrust.dimension != "force": + raise ValueError(f"thrust must be force, got {self.thrust.dimension}") + if self.chamber_pressure.dimension != "pressure": + raise ValueError( + f"chamber_pressure must be pressure, got {self.chamber_pressure.dimension}" + ) + if self.chamber_temp.dimension != "temperature": + raise ValueError( + f"chamber_temp must be temperature, got {self.chamber_temp.dimension}" + ) + if self.exit_pressure.dimension != "pressure": + raise ValueError( + f"exit_pressure must be pressure, got {self.exit_pressure.dimension}" + ) + if self.lstar.dimension != "length": + raise ValueError(f"lstar must be length, got {self.lstar.dimension}") + if self.ambient_pressure is not None and self.ambient_pressure.dimension != "pressure": + raise ValueError( + f"ambient_pressure must be pressure, got {self.ambient_pressure.dimension}" + ) + + # Validate physical constraints + if self.gamma <= 1.0: + raise ValueError(f"gamma must be > 1, got {self.gamma}") + if self.molecular_weight <= 0: + raise ValueError(f"molecular_weight must be > 0, got {self.molecular_weight}") + if self.mixture_ratio <= 0: + raise ValueError(f"mixture_ratio must be > 0, got {self.mixture_ratio}") + if self.contraction_ratio < 1.0: + raise ValueError(f"contraction_ratio must be >= 1, got {self.contraction_ratio}") + if not (0 < self.contraction_angle < 90): + raise ValueError( + f"contraction_angle must be between 0 and 90 degrees, got {self.contraction_angle}" + ) + if not (0 < self.bell_fraction <= 1.0): + raise ValueError(f"bell_fraction must be between 0 and 1, got {self.bell_fraction}") + + @property + def effective_ambient_pressure(self) -> Quantity: + """Return ambient pressure, defaulting to exit pressure if not specified.""" + if self.ambient_pressure is not None: + return self.ambient_pressure + return self.exit_pressure + + +# ============================================================================= +# Output Data Structures +# ============================================================================= + + +@beartype +@dataclass(frozen=True, slots=True) +class EnginePerformance: + """Computed performance metrics for a rocket engine. + + All values are computed from EngineInputs using isentropic flow equations. + + Attributes: + isp: Specific impulse at sea level [s] + isp_vac: Specific impulse in vacuum [s] + cstar: Characteristic velocity [m/s] + exhaust_velocity: Nozzle exit velocity [m/s] + thrust_coeff: Thrust coefficient at sea level [-] + thrust_coeff_vac: Vacuum thrust coefficient [-] + mdot: Total mass flow rate [kg/s] + mdot_ox: Oxidizer mass flow rate [kg/s] + mdot_fuel: Fuel mass flow rate [kg/s] + expansion_ratio: Nozzle expansion ratio Ae/At [-] + exit_mach: Mach number at nozzle exit [-] + """ + + isp: Quantity # seconds + isp_vac: Quantity # seconds + cstar: Quantity # m/s + exhaust_velocity: Quantity # m/s + thrust_coeff: float # dimensionless + thrust_coeff_vac: float # dimensionless + mdot: Quantity # kg/s + mdot_ox: Quantity # kg/s + mdot_fuel: Quantity # kg/s + expansion_ratio: float # dimensionless + exit_mach: float # dimensionless + + +@beartype +@dataclass(frozen=True, slots=True) +class EngineGeometry: + """Computed geometry for a rocket engine. + + All dimensions are computed from EngineInputs and EnginePerformance. + + Attributes: + throat_area: Throat cross-sectional area [m^2] + throat_diameter: Throat diameter [m] + exit_area: Nozzle exit area [m^2] + exit_diameter: Nozzle exit diameter [m] + chamber_area: Chamber cross-sectional area [m^2] + chamber_diameter: Chamber diameter [m] + chamber_volume: Total chamber volume [m^3] + chamber_length: Cylindrical chamber length [m] + nozzle_length: Nozzle length (from throat to exit) [m] + expansion_ratio: Ae/At [-] + contraction_ratio: Ac/At [-] + """ + + throat_area: Quantity + throat_diameter: Quantity + exit_area: Quantity + exit_diameter: Quantity + chamber_area: Quantity + chamber_diameter: Quantity + chamber_volume: Quantity + chamber_length: Quantity + nozzle_length: Quantity + expansion_ratio: float + contraction_ratio: float + + +# ============================================================================= +# Computation Functions +# ============================================================================= + + +@beartype +def compute_performance(inputs: EngineInputs) -> EnginePerformance: + """Compute engine performance from inputs. + + This is a pure function that takes engine inputs and returns computed + performance metrics using isentropic flow equations. + + Args: + inputs: Engine input parameters + + Returns: + Computed performance metrics + """ + # Extract values in SI units + thrust_N = inputs.thrust.to("N").value + pc_Pa = inputs.chamber_pressure.to("Pa").value + Tc_K = inputs.chamber_temp.to("K").value + pe_Pa = inputs.exit_pressure.to("Pa").value + pa_Pa = inputs.effective_ambient_pressure.to("Pa").value + MW = inputs.molecular_weight + gamma = inputs.gamma + MR = inputs.mixture_ratio + + # Compute gas properties + R = specific_gas_constant(MW) + + # Pressure ratios + pe_pc = pe_Pa / pc_Pa + pa_pc = pa_Pa / pc_Pa + + # Exit Mach number from pressure ratio + exit_mach = mach_from_pressure_ratio(pc_Pa / pe_Pa, gamma) + + # Expansion ratio from exit Mach + expansion_ratio = area_ratio_from_mach(exit_mach, gamma) + + # Characteristic velocity + cstar_val = characteristic_velocity(gamma, R, Tc_K) + + # Thrust coefficients + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, expansion_ratio) + Cf_vac = thrust_coefficient_vacuum(gamma, pe_pc, expansion_ratio) + + # Specific impulse + Isp_val = specific_impulse(cstar_val, Cf, G0_SI) + Isp_vac_val = specific_impulse(cstar_val, Cf_vac, G0_SI) + + # Mass flow rate + mdot_val = mass_flow_rate(thrust_N, Isp_val, G0_SI) + mdot_ox_val = mdot_val * MR / (MR + 1.0) + mdot_fuel_val = mdot_val / (MR + 1.0) + + # Exhaust velocity + ue_val = Isp_val * G0_SI + + return EnginePerformance( + isp=seconds(Isp_val), + isp_vac=seconds(Isp_vac_val), + cstar=meters_per_second(cstar_val), + exhaust_velocity=meters_per_second(ue_val), + thrust_coeff=Cf, + thrust_coeff_vac=Cf_vac, + mdot=kg_per_second(mdot_val), + mdot_ox=kg_per_second(mdot_ox_val), + mdot_fuel=kg_per_second(mdot_fuel_val), + expansion_ratio=expansion_ratio, + exit_mach=exit_mach, + ) + + +@beartype +def compute_geometry(inputs: EngineInputs, performance: EnginePerformance) -> EngineGeometry: + """Compute engine geometry from inputs and performance. + + This is a pure function that takes engine inputs and computed performance + to determine physical dimensions. + + Args: + inputs: Engine input parameters + performance: Computed performance metrics + + Returns: + Computed geometry + """ + # Extract values + pc_Pa = inputs.chamber_pressure.to("Pa").value + mdot_val = performance.mdot.to("kg/s").value + cstar_val = performance.cstar.to("m/s").value + lstar_m = inputs.lstar.to("m").value + expansion_ratio = performance.expansion_ratio + contraction_ratio = inputs.contraction_ratio + contraction_angle_rad = math.radians(inputs.contraction_angle) + bell_fraction = inputs.bell_fraction + + # Throat geometry + At = throat_area(mdot_val, cstar_val, pc_Pa) + Dt = diameter_from_area(At) + Rt = Dt / 2.0 + + # Exit geometry + Ae = At * expansion_ratio + De = diameter_from_area(Ae) + Re = De / 2.0 + + # Chamber geometry + Ac = At * contraction_ratio + Dc = diameter_from_area(Ac) + Rc = Dc / 2.0 + + # Chamber volume from L* + Vc = chamber_volume(lstar_m, At) + + # Cylindrical chamber length + Lcyl = cylindrical_chamber_length(Vc, Ac, Rc, Rt, contraction_angle_rad) + # Ensure positive length + Lcyl = max(Lcyl, 0.0) + + # Nozzle length (bell nozzle) + Ln = bell_nozzle_length(Rt, Re, bell_fraction) + + return EngineGeometry( + throat_area=square_meters(At), + throat_diameter=meters(Dt), + exit_area=square_meters(Ae), + exit_diameter=meters(De), + chamber_area=square_meters(Ac), + chamber_diameter=meters(Dc), + chamber_volume=Quantity(Vc, "m^3", "volume"), + chamber_length=meters(Lcyl), + nozzle_length=meters(Ln), + expansion_ratio=expansion_ratio, + contraction_ratio=contraction_ratio, + ) + + +@beartype +def design_engine(inputs: EngineInputs) -> tuple[EnginePerformance, EngineGeometry]: + """Complete engine design from inputs. + + Convenience function that computes both performance and geometry. + + Args: + inputs: Engine input parameters + + Returns: + Tuple of (performance, geometry) + """ + performance = compute_performance(inputs) + geometry = compute_geometry(inputs, performance) + return performance, geometry + + +# ============================================================================= +# Analysis Functions +# ============================================================================= + + +@beartype +def thrust_at_altitude( + inputs: EngineInputs, + performance: EnginePerformance, + geometry: EngineGeometry, + ambient_pressure: Quantity, +) -> Quantity: + """Calculate thrust at a given ambient pressure (altitude). + + Args: + inputs: Engine input parameters + performance: Computed performance (used for expansion ratio) + geometry: Computed geometry (used for exit area) + ambient_pressure: Ambient pressure at altitude + + Returns: + Thrust at the specified altitude + """ + pc_Pa = inputs.chamber_pressure.to("Pa").value + pe_Pa = inputs.exit_pressure.to("Pa").value + pa_Pa = ambient_pressure.to("Pa").value + gamma = inputs.gamma + cstar_val = performance.cstar.to("m/s").value + mdot_val = performance.mdot.to("kg/s").value + expansion_ratio = performance.expansion_ratio + + pe_pc = pe_Pa / pc_Pa + pa_pc = pa_Pa / pc_Pa + + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, expansion_ratio) + thrust_N = mdot_val * cstar_val * Cf + + return Quantity(thrust_N, "N", "force") + + +@beartype +def isp_at_altitude( + inputs: EngineInputs, + performance: EnginePerformance, + ambient_pressure: Quantity, +) -> Quantity: + """Calculate specific impulse at a given ambient pressure (altitude). + + Args: + inputs: Engine input parameters + performance: Computed performance + ambient_pressure: Ambient pressure at altitude + + Returns: + Specific impulse at the specified altitude + """ + pc_Pa = inputs.chamber_pressure.to("Pa").value + pe_Pa = inputs.exit_pressure.to("Pa").value + pa_Pa = ambient_pressure.to("Pa").value + gamma = inputs.gamma + cstar_val = performance.cstar.to("m/s").value + expansion_ratio = performance.expansion_ratio + + pe_pc = pe_Pa / pc_Pa + pa_pc = pa_Pa / pc_Pa + + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, expansion_ratio) + Isp = specific_impulse(cstar_val, Cf, G0_SI) + + return seconds(Isp) + + +# ============================================================================= +# Summary and Display +# ============================================================================= + + +@beartype +def format_performance_summary(inputs: EngineInputs, performance: EnginePerformance) -> str: + """Format a human-readable performance summary. + + Args: + inputs: Engine input parameters + performance: Computed performance + + Returns: + Formatted string summary + """ + name = inputs.name or "Unnamed Engine" + lines = [ + f"{'=' * 60}", + f"ENGINE PERFORMANCE SUMMARY: {name}", + f"{'=' * 60}", + "", + "INPUTS:", + f" Thrust (SL): {inputs.thrust}", + f" Chamber Pressure: {inputs.chamber_pressure}", + f" Chamber Temp: {inputs.chamber_temp}", + f" Exit Pressure: {inputs.exit_pressure}", + f" Molecular Weight: {inputs.molecular_weight:.2f} kg/kmol", + f" Gamma: {inputs.gamma:.3f}", + f" Mixture Ratio: {inputs.mixture_ratio:.2f}", + "", + "PERFORMANCE:", + f" Isp (SL): {performance.isp.value:.1f} s", + f" Isp (Vac): {performance.isp_vac.value:.1f} s", + f" C*: {performance.cstar.value:.1f} m/s", + f" Exit Velocity: {performance.exhaust_velocity.value:.1f} m/s", + f" Thrust Coeff (SL): {performance.thrust_coeff:.3f}", + f" Thrust Coeff (Vac): {performance.thrust_coeff_vac:.3f}", + f" Exit Mach: {performance.exit_mach:.2f}", + "", + "MASS FLOW:", + f" Total: {performance.mdot.value:.3f} kg/s", + f" Oxidizer: {performance.mdot_ox.value:.3f} kg/s", + f" Fuel: {performance.mdot_fuel.value:.3f} kg/s", + "", + f" Expansion Ratio: {performance.expansion_ratio:.2f}", + f"{'=' * 60}", + ] + return "\n".join(lines) + + +@beartype +def format_geometry_summary(inputs: EngineInputs, geometry: EngineGeometry) -> str: + """Format a human-readable geometry summary. + + Args: + inputs: Engine input parameters + geometry: Computed geometry + + Returns: + Formatted string summary + """ + name = inputs.name or "Unnamed Engine" + lines = [ + f"{'=' * 60}", + f"ENGINE GEOMETRY SUMMARY: {name}", + f"{'=' * 60}", + "", + "THROAT:", + f" Area: {geometry.throat_area.value * 1e4:.4f} cm^2", + f" Diameter: {geometry.throat_diameter.value * 100:.3f} cm", + "", + "EXIT:", + f" Area: {geometry.exit_area.value * 1e4:.2f} cm^2", + f" Diameter: {geometry.exit_diameter.value * 100:.2f} cm", + "", + "CHAMBER:", + f" Area: {geometry.chamber_area.value * 1e4:.2f} cm^2", + f" Diameter: {geometry.chamber_diameter.value * 100:.2f} cm", + f" Volume: {geometry.chamber_volume.value * 1e6:.1f} cm^3", + f" Length (cyl): {geometry.chamber_length.value * 100:.2f} cm", + "", + "NOZZLE:", + f" Length: {geometry.nozzle_length.value * 100:.2f} cm", + "", + "RATIOS:", + f" Expansion (Ae/At): {geometry.expansion_ratio:.2f}", + f" Contraction (Ac/At):{geometry.contraction_ratio:.2f}", + f"{'=' * 60}", + ] + return "\n".join(lines) + diff --git a/openrocketengine/examples/__init__.py b/openrocketengine/examples/__init__.py new file mode 100644 index 0000000..9e1f835 --- /dev/null +++ b/openrocketengine/examples/__init__.py @@ -0,0 +1,2 @@ +"""Example scripts for OpenRocketEngine.""" + diff --git a/openrocketengine/examples/basic_engine.py b/openrocketengine/examples/basic_engine.py new file mode 100644 index 0000000..deb7437 --- /dev/null +++ b/openrocketengine/examples/basic_engine.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python +"""Basic engine design example for OpenRocketEngine. + +This example demonstrates the complete workflow for designing a small +liquid rocket engine: + +1. Define engine inputs (thrust, pressures, temperatures, etc.) +2. Compute performance metrics (Isp, c*, Cf, mass flow rates) +3. Compute geometry (throat, chamber, exit dimensions) +4. Generate nozzle contour +5. Visualize the design +6. Export contour for CAD + +The example engine is similar to a small pressure-fed engine suitable +for a student rocket project. +""" + +from openrocketengine.engine import ( + EngineInputs, + compute_geometry, + compute_performance, + format_geometry_summary, + format_performance_summary, +) +from openrocketengine.nozzle import ( + full_chamber_contour, + generate_nozzle_from_geometry, +) +from openrocketengine.plotting import ( + plot_engine_cross_section, + plot_engine_dashboard, + plot_nozzle_contour, + plot_performance_vs_altitude, +) +from openrocketengine.units import kelvin, megapascals, meters, newtons, pascals + + +def main() -> None: + """Run the basic engine design example.""" + print("=" * 70) + print("OpenRocketEngine - Basic Engine Design Example") + print("=" * 70) + print() + + # ========================================================================= + # Step 1: Define Engine Inputs + # ========================================================================= + # + # We're designing a small pressure-fed engine with the following specs: + # - ~5 kN (1100 lbf) sea-level thrust + # - LOX/Ethanol propellants (assumed combustion properties) + # - Pressure-fed, so moderate chamber pressure + # + # The thermochemical properties (Tc, gamma, MW) would normally come from + # NASA CEA or similar tool. Here we use representative values for + # LOX/Ethanol at O/F = 1.3 + + print("Step 1: Defining engine inputs...") + print() + + inputs = EngineInputs( + name="Student Engine Mk1", + # Performance targets + thrust=newtons(5000), # 5 kN sea-level thrust + # Chamber conditions + chamber_pressure=megapascals(2.0), # 2 MPa (~290 psi) + chamber_temp=kelvin(3200), # Flame temperature from CEA + exit_pressure=pascals(101325), # Expanded to sea level + # Propellant properties (from CEA for LOX/Ethanol) + molecular_weight=21.5, # kg/kmol + gamma=1.22, # Cp/Cv + mixture_ratio=1.3, # O/F mass ratio + # Chamber geometry parameters + lstar=meters(1.0), # Characteristic length (typical for biprop) + contraction_ratio=4.0, # Ac/At + contraction_angle=45.0, # degrees + bell_fraction=0.8, # 80% bell nozzle + ) + + print(f" Engine Name: {inputs.name}") + print(f" Design Thrust: {inputs.thrust}") + print(f" Chamber Pressure: {inputs.chamber_pressure}") + print(f" Chamber Temp: {inputs.chamber_temp}") + print(f" Exit Pressure: {inputs.exit_pressure}") + print(f" Molecular Weight: {inputs.molecular_weight} kg/kmol") + print(f" Gamma: {inputs.gamma}") + print(f" Mixture Ratio: {inputs.mixture_ratio}") + print() + + # ========================================================================= + # Step 2: Compute Performance + # ========================================================================= + + print("Step 2: Computing performance...") + print() + + performance = compute_performance(inputs) + + print(f" Specific Impulse (SL): {performance.isp.value:.1f} s") + print(f" Specific Impulse (Vac): {performance.isp_vac.value:.1f} s") + print(f" Characteristic Velocity: {performance.cstar.value:.0f} m/s") + print(f" Thrust Coefficient (SL): {performance.thrust_coeff:.3f}") + print(f" Exit Mach Number: {performance.exit_mach:.2f}") + print(f" Expansion Ratio: {performance.expansion_ratio:.1f}") + print() + print(f" Total Mass Flow: {performance.mdot.value:.3f} kg/s") + print(f" Oxidizer Flow: {performance.mdot_ox.value:.3f} kg/s") + print(f" Fuel Flow: {performance.mdot_fuel.value:.3f} kg/s") + print() + + # ========================================================================= + # Step 3: Compute Geometry + # ========================================================================= + + print("Step 3: Computing geometry...") + print() + + geometry = compute_geometry(inputs, performance) + + # Convert to more convenient units for display + Dt_mm = geometry.throat_diameter.to("m").value * 1000 + De_mm = geometry.exit_diameter.to("m").value * 1000 + Dc_mm = geometry.chamber_diameter.to("m").value * 1000 + Lc_mm = geometry.chamber_length.to("m").value * 1000 + Ln_mm = geometry.nozzle_length.to("m").value * 1000 + + print(f" Throat Diameter: {Dt_mm:.1f} mm") + print(f" Exit Diameter: {De_mm:.1f} mm") + print(f" Chamber Diameter: {Dc_mm:.1f} mm") + print(f" Chamber Length: {Lc_mm:.1f} mm") + print(f" Nozzle Length: {Ln_mm:.1f} mm") + print(f" Expansion Ratio: {geometry.expansion_ratio:.1f}") + print(f" Contraction Ratio: {geometry.contraction_ratio:.1f}") + print() + + # ========================================================================= + # Step 4: Generate Nozzle Contour + # ========================================================================= + + print("Step 4: Generating nozzle contour...") + print() + + # Generate just the divergent nozzle section + nozzle_contour = generate_nozzle_from_geometry( + geometry, bell_fraction=inputs.bell_fraction, num_points=100 + ) + + print(f" Contour Type: {nozzle_contour.contour_type}") + print(f" Number of Points: {len(nozzle_contour.x)}") + print(f" Nozzle Length: {nozzle_contour.length * 1000:.1f} mm") + print() + + # Generate full chamber contour (chamber + convergent + divergent) + full_contour = full_chamber_contour( + inputs, geometry, nozzle_contour, num_chamber_points=50, num_convergent_points=30 + ) + + print(f" Full Contour Type: {full_contour.contour_type}") + print(f" Total Points: {len(full_contour.x)}") + print(f" Total Length: {full_contour.length * 1000:.1f} mm") + print() + + # ========================================================================= + # Step 5: Export Contour to CSV + # ========================================================================= + + print("Step 5: Exporting contour to CSV...") + print() + + nozzle_contour.to_csv("nozzle_contour.csv") + full_contour.to_csv("full_chamber_contour.csv") + + print(" Saved: nozzle_contour.csv") + print(" Saved: full_chamber_contour.csv") + print() + + # ========================================================================= + # Step 6: Print Summaries + # ========================================================================= + + print("Step 6: Full summaries...") + print() + print(format_performance_summary(inputs, performance)) + print() + print(format_geometry_summary(inputs, geometry)) + print() + + # ========================================================================= + # Step 7: Create Visualizations + # ========================================================================= + + print("Step 7: Creating visualizations...") + print() + + # Engine cross-section + fig1 = plot_engine_cross_section( + geometry, full_contour, inputs, show_dimensions=True, title=f"{inputs.name} Cross-Section" + ) + fig1.savefig("engine_cross_section.png", dpi=150, bbox_inches="tight") + print(" Saved: engine_cross_section.png") + + # Nozzle contour detail + fig2 = plot_nozzle_contour(nozzle_contour, title=f"{inputs.name} Nozzle Contour") + fig2.savefig("nozzle_contour.png", dpi=150, bbox_inches="tight") + print(" Saved: nozzle_contour.png") + + # Performance vs altitude + fig3 = plot_performance_vs_altitude(inputs, performance, geometry, max_altitude_km=80) + fig3.savefig("altitude_performance.png", dpi=150, bbox_inches="tight") + print(" Saved: altitude_performance.png") + + # Complete dashboard + fig4 = plot_engine_dashboard(inputs, performance, geometry, full_contour) + fig4.savefig("engine_dashboard.png", dpi=150, bbox_inches="tight") + print(" Saved: engine_dashboard.png") + + print() + print("=" * 70) + print("Design complete!") + print("=" * 70) + + +if __name__ == "__main__": + main() + diff --git a/openrocketengine/isentropic.py b/openrocketengine/isentropic.py new file mode 100644 index 0000000..7840e28 --- /dev/null +++ b/openrocketengine/isentropic.py @@ -0,0 +1,633 @@ +"""Isentropic flow equations for rocket engine analysis. + +This module contains the core thermodynamic calculations for rocket engine +performance analysis. All functions are pure (no side effects) and +numba-accelerated for performance. + +The equations are based on isentropic flow relations for ideal gases, +which form the foundation of rocket propulsion analysis. + +References: + - Sutton & Biblarz, "Rocket Propulsion Elements", 9th Ed. + - Huzel & Huang, "Modern Engineering for Design of Liquid-Propellant Rocket Engines" + - Hill & Peterson, "Mechanics and Thermodynamics of Propulsion", 2nd Ed. +""" + +import math + +import numba +import numpy as np +from numpy.typing import NDArray + +# ============================================================================= +# Constants +# ============================================================================= + +# Standard gravity acceleration +G0_SI: float = 9.80665 # m/s^2 +G0_IMP: float = 32.174 # ft/s^2 + +# Universal gas constant +R_UNIVERSAL_SI: float = 8314.46 # J/(kmol·K) +R_UNIVERSAL_IMP: float = 1545.35 # ft·lbf/(lbmol·R) + + +# ============================================================================= +# Core Isentropic Flow Functions (Numba Accelerated) +# ============================================================================= + + +@numba.njit(cache=True) +def specific_gas_constant(molecular_weight: float) -> float: + """Calculate specific gas constant from molecular weight. + + Args: + molecular_weight: Molecular weight of the gas [kg/kmol] + + Returns: + Specific gas constant R [J/(kg·K)] + """ + return R_UNIVERSAL_SI / molecular_weight + + +@numba.njit(cache=True) +def characteristic_velocity(gamma: float, R: float, Tc: float) -> float: + """Calculate characteristic velocity (c*). + + c* is a measure of the energy available from the combustion process, + independent of nozzle performance. + + Args: + gamma: Ratio of specific heats (Cp/Cv) [-] + R: Specific gas constant [J/(kg·K)] + Tc: Chamber (stagnation) temperature [K] + + Returns: + Characteristic velocity [m/s] + """ + # c* = sqrt(gamma * R * Tc) / (gamma * sqrt((2/(gamma+1))^((gamma+1)/(gamma-1)))) + term1 = math.sqrt(gamma * R * Tc) + term2 = gamma * math.sqrt((2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (gamma - 1.0))) + return term1 / term2 + + +@numba.njit(cache=True) +def thrust_coefficient( + gamma: float, pe_pc: float, pa_pc: float, expansion_ratio: float +) -> float: + """Calculate thrust coefficient (Cf). + + Cf characterizes the nozzle's ability to convert thermal energy + into directed kinetic energy. + + Args: + gamma: Ratio of specific heats [-] + pe_pc: Exit pressure / chamber pressure ratio [-] + pa_pc: Ambient pressure / chamber pressure ratio [-] + expansion_ratio: Nozzle exit area / throat area (Ae/At) [-] + + Returns: + Thrust coefficient [-] + """ + # Momentum thrust term + gm1 = gamma - 1.0 + gp1 = gamma + 1.0 + exponent = gm1 / gamma + + term1 = 2.0 * gamma**2 / gm1 + term2 = (2.0 / gp1) ** (gp1 / gm1) + term3 = 1.0 - pe_pc**exponent + + Cf_momentum = math.sqrt(term1 * term2 * term3) + + # Pressure thrust term + Cf_pressure = (pe_pc - pa_pc) * expansion_ratio + + return Cf_momentum + Cf_pressure + + +@numba.njit(cache=True) +def thrust_coefficient_vacuum(gamma: float, pe_pc: float, expansion_ratio: float) -> float: + """Calculate vacuum thrust coefficient (Cf_vac). + + Args: + gamma: Ratio of specific heats [-] + pe_pc: Exit pressure / chamber pressure ratio [-] + expansion_ratio: Nozzle exit area / throat area (Ae/At) [-] + + Returns: + Vacuum thrust coefficient [-] + """ + return thrust_coefficient(gamma, pe_pc, 0.0, expansion_ratio) + + +@numba.njit(cache=True) +def specific_impulse(cstar: float, Cf: float, g0: float = G0_SI) -> float: + """Calculate specific impulse (Isp). + + Isp is the key performance metric for rocket engines, representing + the thrust produced per unit weight flow rate of propellant. + + Args: + cstar: Characteristic velocity [m/s] + Cf: Thrust coefficient [-] + g0: Standard gravity [m/s^2], default 9.80665 + + Returns: + Specific impulse [s] + """ + return cstar * Cf / g0 + + +@numba.njit(cache=True) +def exhaust_velocity(gamma: float, R: float, Tc: float, pe_pc: float) -> float: + """Calculate exhaust velocity (ue). + + This is the velocity of the exhaust gases at the nozzle exit + for isentropic expansion. + + Args: + gamma: Ratio of specific heats [-] + R: Specific gas constant [J/(kg·K)] + Tc: Chamber temperature [K] + pe_pc: Exit pressure / chamber pressure ratio [-] + + Returns: + Exhaust velocity [m/s] + """ + gm1 = gamma - 1.0 + exponent = gm1 / gamma + + term1 = 2.0 * gamma * R * Tc / gm1 + term2 = 1.0 - pe_pc**exponent + + return math.sqrt(term1 * term2) + + +@numba.njit(cache=True) +def mass_flow_rate(thrust: float, Isp: float, g0: float = G0_SI) -> float: + """Calculate total mass flow rate from thrust and Isp. + + Args: + thrust: Engine thrust [N] + Isp: Specific impulse [s] + g0: Standard gravity [m/s^2] + + Returns: + Mass flow rate [kg/s] + """ + return thrust / (Isp * g0) + + +@numba.njit(cache=True) +def mass_flow_rate_from_throat( + pc: float, At: float, gamma: float, R: float, Tc: float +) -> float: + """Calculate mass flow rate from throat conditions. + + Uses the choked flow condition at the throat. + + Args: + pc: Chamber pressure [Pa] + At: Throat area [m^2] + gamma: Ratio of specific heats [-] + R: Specific gas constant [J/(kg·K)] + Tc: Chamber temperature [K] + + Returns: + Mass flow rate [kg/s] + """ + gp1 = gamma + 1.0 + gm1 = gamma - 1.0 + + term1 = pc * At + term2 = gamma / (R * Tc) + term3 = (2.0 / gp1) ** (gp1 / gm1) + + return term1 * math.sqrt(term2 * term3) + + +@numba.njit(cache=True) +def throat_area(mdot: float, cstar: float, pc: float) -> float: + """Calculate required throat area. + + Args: + mdot: Mass flow rate [kg/s] + cstar: Characteristic velocity [m/s] + pc: Chamber pressure [Pa] + + Returns: + Throat area [m^2] + """ + return mdot * cstar / pc + + +@numba.njit(cache=True) +def area_from_diameter(diameter: float) -> float: + """Calculate circular area from diameter. + + Args: + diameter: Diameter [m] + + Returns: + Area [m^2] + """ + return math.pi * (diameter / 2.0) ** 2 + + +@numba.njit(cache=True) +def diameter_from_area(area: float) -> float: + """Calculate diameter from circular area. + + Args: + area: Area [m^2] + + Returns: + Diameter [m] + """ + return 2.0 * math.sqrt(area / math.pi) + + +# ============================================================================= +# Mach Number Relations +# ============================================================================= + + +@numba.njit(cache=True) +def mach_from_pressure_ratio(pc_p: float, gamma: float) -> float: + """Calculate Mach number from stagnation-to-static pressure ratio. + + Args: + pc_p: Chamber (stagnation) pressure / local static pressure [-] + gamma: Ratio of specific heats [-] + + Returns: + Mach number [-] + """ + gm1 = gamma - 1.0 + exponent = gm1 / gamma + + return math.sqrt((2.0 / gm1) * (pc_p**exponent - 1.0)) + + +@numba.njit(cache=True) +def pressure_ratio_from_mach(M: float, gamma: float) -> float: + """Calculate stagnation-to-static pressure ratio from Mach number. + + Args: + M: Mach number [-] + gamma: Ratio of specific heats [-] + + Returns: + pc/p ratio [-] + """ + gm1 = gamma - 1.0 + exponent = gamma / gm1 + + return (1.0 + gm1 / 2.0 * M**2) ** exponent + + +@numba.njit(cache=True) +def temperature_ratio_from_mach(M: float, gamma: float) -> float: + """Calculate stagnation-to-static temperature ratio from Mach number. + + Args: + M: Mach number [-] + gamma: Ratio of specific heats [-] + + Returns: + Tc/T ratio [-] + """ + return 1.0 + (gamma - 1.0) / 2.0 * M**2 + + +@numba.njit(cache=True) +def area_ratio_from_mach(M: float, gamma: float) -> float: + """Calculate area ratio (A/A*) from Mach number. + + A* is the critical (sonic) area, i.e., the throat area for choked flow. + + Args: + M: Mach number [-] + gamma: Ratio of specific heats [-] + + Returns: + Area ratio A/A* [-] + """ + if M <= 0.0: + return float("inf") + + gm1 = gamma - 1.0 + gp1 = gamma + 1.0 + exponent = gp1 / (2.0 * gm1) + + term1 = 1.0 / M + term2 = (2.0 / gp1) * (1.0 + gm1 / 2.0 * M**2) + + return term1 * term2**exponent + + +@numba.njit(cache=True) +def mach_from_area_ratio_supersonic(area_ratio: float, gamma: float) -> float: + """Calculate supersonic Mach number from area ratio using Newton-Raphson. + + For a given A/A* > 1, there are two solutions: subsonic and supersonic. + This function returns the supersonic solution (M > 1). + + Args: + area_ratio: Area ratio A/A* [-], must be >= 1 + gamma: Ratio of specific heats [-] + + Returns: + Supersonic Mach number [-] + """ + if area_ratio < 1.0: + return 1.0 # At throat + + # Initial guess for supersonic flow + M = 2.0 + area_ratio / 5.0 + + # Newton-Raphson iteration + for _ in range(50): + f = area_ratio_from_mach(M, gamma) - area_ratio + + # Numerical derivative + dM = 1e-8 + df = (area_ratio_from_mach(M + dM, gamma) - area_ratio_from_mach(M - dM, gamma)) / ( + 2.0 * dM + ) + + if abs(df) < 1e-12: + break + + M_new = M - f / df + + if M_new < 1.0: + M_new = 1.0 + 0.1 + + if abs(M_new - M) < 1e-10: + break + + M = M_new + + return M + + +@numba.njit(cache=True) +def mach_from_area_ratio_subsonic(area_ratio: float, gamma: float) -> float: + """Calculate subsonic Mach number from area ratio using Newton-Raphson. + + For a given A/A* > 1, there are two solutions: subsonic and supersonic. + This function returns the subsonic solution (M < 1). + + Args: + area_ratio: Area ratio A/A* [-], must be >= 1 + gamma: Ratio of specific heats [-] + + Returns: + Subsonic Mach number [-] + """ + if area_ratio < 1.0: + return 1.0 + + # Initial guess for subsonic flow + M = 0.5 + + # Newton-Raphson iteration + for _ in range(50): + f = area_ratio_from_mach(M, gamma) - area_ratio + + # Numerical derivative + dM = 1e-8 + df = (area_ratio_from_mach(M + dM, gamma) - area_ratio_from_mach(M - dM, gamma)) / ( + 2.0 * dM + ) + + if abs(df) < 1e-12: + break + + M_new = M - f / df + + if M_new > 1.0: + M_new = 0.99 + if M_new < 0.0: + M_new = 0.01 + + if abs(M_new - M) < 1e-10: + break + + M = M_new + + return M + + +# ============================================================================= +# Throat and Exit Conditions +# ============================================================================= + + +@numba.njit(cache=True) +def throat_temperature(Tc: float, gamma: float) -> float: + """Calculate throat (critical) temperature. + + Args: + Tc: Chamber temperature [K] + gamma: Ratio of specific heats [-] + + Returns: + Throat temperature [K] + """ + return Tc / (1.0 + (gamma - 1.0) / 2.0) + + +@numba.njit(cache=True) +def throat_pressure(pc: float, gamma: float) -> float: + """Calculate throat (critical) pressure. + + Args: + pc: Chamber pressure [Pa] + gamma: Ratio of specific heats [-] + + Returns: + Throat pressure [Pa] + """ + exponent = gamma / (gamma - 1.0) + return pc * (2.0 / (gamma + 1.0)) ** exponent + + +@numba.njit(cache=True) +def expansion_ratio_from_pressure_ratio(pc_pe: float, gamma: float) -> float: + """Calculate nozzle expansion ratio from chamber-to-exit pressure ratio. + + Args: + pc_pe: Chamber pressure / exit pressure [-] + gamma: Ratio of specific heats [-] + + Returns: + Expansion ratio (Ae/At) [-] + """ + # First get exit Mach number + Me = mach_from_pressure_ratio(pc_pe, gamma) + # Then get area ratio + return area_ratio_from_mach(Me, gamma) + + +@numba.njit(cache=True) +def exit_pressure_from_expansion_ratio( + expansion_ratio: float, pc: float, gamma: float +) -> float: + """Calculate exit pressure from expansion ratio. + + Args: + expansion_ratio: Nozzle expansion ratio (Ae/At) [-] + pc: Chamber pressure [Pa] + gamma: Ratio of specific heats [-] + + Returns: + Exit pressure [Pa] + """ + # Get exit Mach number (supersonic solution) + Me = mach_from_area_ratio_supersonic(expansion_ratio, gamma) + # Get pressure ratio + pc_pe = pressure_ratio_from_mach(Me, gamma) + return pc / pc_pe + + +# ============================================================================= +# Chamber Geometry +# ============================================================================= + + +@numba.njit(cache=True) +def chamber_volume(lstar: float, At: float) -> float: + """Calculate chamber volume from L* and throat area. + + L* (characteristic length) is defined as the chamber volume divided + by the throat area: L* = Vc / At + + Args: + lstar: Characteristic length [m] + At: Throat area [m^2] + + Returns: + Chamber volume [m^3] + """ + return lstar * At + + +@numba.njit(cache=True) +def cylindrical_chamber_length( + Vc: float, Ac: float, Rc: float, Rt: float, contraction_angle: float +) -> float: + """Calculate length of cylindrical section of chamber. + + Accounts for the converging section geometry. + + Args: + Vc: Chamber volume [m^3] + Ac: Chamber cross-sectional area [m^2] + Rc: Chamber radius [m] + Rt: Throat radius [m] + contraction_angle: Convergence half-angle [radians] + + Returns: + Cylindrical section length [m] + """ + # Volume of converging cone section + cone_length = (Rc - Rt) / math.tan(contraction_angle) + # Approximate cylindrical length (subtract converging section contribution) + return Vc / Ac - 0.5 * cone_length + + +@numba.njit(cache=True) +def conical_nozzle_length(Rt: float, Re: float, half_angle: float) -> float: + """Calculate length of a conical nozzle. + + Args: + Rt: Throat radius [m] + Re: Exit radius [m] + half_angle: Nozzle half-angle [radians] + + Returns: + Nozzle length [m] + """ + return (Re - Rt) / math.tan(half_angle) + + +@numba.njit(cache=True) +def bell_nozzle_length( + Rt: float, Re: float, bell_fraction: float = 0.8, reference_angle: float = 0.2618 +) -> float: + """Calculate length of a bell (parabolic) nozzle. + + Bell nozzles are typically specified as a percentage of the length + of a 15-degree conical nozzle with the same expansion ratio. + + Args: + Rt: Throat radius [m] + Re: Exit radius [m] + bell_fraction: Length as fraction of 15° cone (e.g., 0.8 for 80% bell) + reference_angle: Reference cone half-angle [radians], default 15° = 0.2618 + + Returns: + Nozzle length [m] + """ + conical_length = conical_nozzle_length(Rt, Re, reference_angle) + return conical_length * bell_fraction + + +# ============================================================================= +# Vectorized Functions for Parametric Studies +# ============================================================================= + + +@numba.njit(cache=True, parallel=True) +def thrust_coefficient_sweep( + gamma: float, + pe_pc: float, + pa_pc_array: NDArray[np.float64], + expansion_ratio: float, +) -> NDArray[np.float64]: + """Calculate thrust coefficient for array of ambient pressures. + + Useful for altitude performance analysis. + + Args: + gamma: Ratio of specific heats [-] + pe_pc: Exit pressure / chamber pressure ratio [-] + pa_pc_array: Array of ambient pressure / chamber pressure ratios [-] + expansion_ratio: Nozzle expansion ratio [-] + + Returns: + Array of thrust coefficients [-] + """ + n = len(pa_pc_array) + result = np.empty(n, dtype=np.float64) + + for i in numba.prange(n): + result[i] = thrust_coefficient(gamma, pe_pc, pa_pc_array[i], expansion_ratio) + + return result + + +@numba.njit(cache=True) +def area_ratio_sweep( + mach_array: NDArray[np.float64], gamma: float +) -> NDArray[np.float64]: + """Calculate area ratios for array of Mach numbers. + + Args: + mach_array: Array of Mach numbers [-] + gamma: Ratio of specific heats [-] + + Returns: + Array of area ratios [-] + """ + n = len(mach_array) + result = np.empty(n, dtype=np.float64) + + for i in range(n): + result[i] = area_ratio_from_mach(mach_array[i], gamma) + + return result + diff --git a/openrocketengine/nozzle.py b/openrocketengine/nozzle.py new file mode 100644 index 0000000..26d250a --- /dev/null +++ b/openrocketengine/nozzle.py @@ -0,0 +1,425 @@ +"""Nozzle contour generation for rocket engines. + +This module provides functions to generate nozzle contours for various +nozzle types including: +- Conical nozzles (simple 15° half-angle) +- Rao parabolic bell nozzles (optimized for performance) + +The contours can be exported to CSV for CAD import. + +References: + - Rao, G.V.R., "Exhaust Nozzle Contour for Optimum Thrust", + Jet Propulsion, Vol. 28, No. 6, 1958 + - Huzel & Huang, "Modern Engineering for Design of Liquid-Propellant + Rocket Engines", Chapter 4 +""" + +import math +from dataclasses import dataclass +from pathlib import Path + +import numpy as np +from beartype import beartype +from numpy.typing import NDArray + +from openrocketengine.engine import EngineGeometry, EngineInputs +from openrocketengine.units import Quantity + +# ============================================================================= +# Nozzle Contour Data Structure +# ============================================================================= + + +@beartype +@dataclass(frozen=True) +class NozzleContour: + """Nozzle contour defined by axial and radial coordinates. + + The contour represents the inner wall of the nozzle from the chamber + through the throat to the exit. Coordinates are in meters. + + Attributes: + x: Axial positions [m], with x=0 at throat + y: Radial positions [m] (radius, not diameter) + contour_type: Type of contour ("rao_bell", "conical", etc.) + """ + + x: NDArray[np.float64] + y: NDArray[np.float64] + contour_type: str + + def __post_init__(self) -> None: + """Validate contour data.""" + if len(self.x) != len(self.y): + raise ValueError( + f"x and y arrays must have same length, got {len(self.x)} and {len(self.y)}" + ) + if len(self.x) < 2: + raise ValueError("Contour must have at least 2 points") + + def to_csv(self, path: str | Path, include_header: bool = True) -> None: + """Export contour to CSV file for CAD import. + + Args: + path: Output file path + include_header: Whether to include column headers + """ + path = Path(path) + with path.open("w") as f: + if include_header: + f.write("x_m,y_m,x_mm,y_mm\n") + for xi, yi in zip(self.x, self.y, strict=True): + f.write(f"{xi:.8f},{yi:.8f},{xi * 1000:.6f},{yi * 1000:.6f}\n") + + def to_arrays_mm(self) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Return contour coordinates in millimeters. + + Returns: + Tuple of (x_mm, y_mm) arrays + """ + return self.x * 1000, self.y * 1000 + + @property + def length(self) -> float: + """Total axial length of the contour [m].""" + return float(self.x[-1] - self.x[0]) + + @property + def throat_radius(self) -> float: + """Radius at throat (minimum y value) [m].""" + return float(np.min(self.y)) + + @property + def exit_radius(self) -> float: + """Radius at exit [m].""" + return float(self.y[-1]) + + @property + def inlet_radius(self) -> float: + """Radius at inlet [m].""" + return float(self.y[0]) + + +# ============================================================================= +# Rao Bell Nozzle Contour +# ============================================================================= + + +@beartype +def rao_bell_contour( + throat_radius: Quantity, + exit_radius: Quantity, + expansion_ratio: float, + bell_fraction: float = 0.8, + num_points: int = 100, +) -> NozzleContour: + """Generate a Rao parabolic bell nozzle contour. + + The Rao bell nozzle uses a parabolic approximation to the ideal + thrust-optimized contour. It consists of: + 1. A circular arc leaving the throat (radius = 0.382 * Rt) + 2. A parabolic section to the exit + + The bell_fraction parameter specifies the length as a fraction of + an equivalent 15° conical nozzle. + + Args: + throat_radius: Throat radius [length] + exit_radius: Exit radius [length] + expansion_ratio: Area ratio Ae/At [-] + bell_fraction: Length as fraction of 15° cone (typically 0.8) + num_points: Number of points in the contour + + Returns: + NozzleContour with the bell nozzle shape + """ + # Convert to SI + Rt = throat_radius.to("m").value + Re = exit_radius.to("m").value + + # Rao parameters (from empirical correlations) + # Initial angle leaving throat depends on expansion ratio + # Final angle at exit also depends on expansion ratio + # These are approximations from Rao's paper and Huzel & Huang + + # Throat circular arc radius + Rn = 0.382 * Rt # Radius of curvature leaving throat + + # Reference conical nozzle length (15° half-angle) + Lc_15 = (Re - Rt) / math.tan(math.radians(15)) + + # Bell nozzle length + Ln = Lc_15 * bell_fraction + + # Initial and final angles (empirical fits from Rao curves) + # These depend on expansion ratio and bell fraction + theta_n = _rao_initial_angle(expansion_ratio, bell_fraction) + theta_e = _rao_exit_angle(expansion_ratio, bell_fraction) + + # Convert to radians + theta_n_rad = math.radians(theta_n) + theta_e_rad = math.radians(theta_e) + + # Generate contour points + + # Point N: End of throat circular arc + # x_N is relative to throat center + x_N = Rn * math.sin(theta_n_rad) + y_N = Rt + Rn * (1 - math.cos(theta_n_rad)) + + # Point E: Exit + x_E = Ln + y_E = Re + + # Generate throat arc (from throat to point N) + n_arc = num_points // 4 + theta_arc = np.linspace(0, theta_n_rad, n_arc) + x_arc = Rn * np.sin(theta_arc) + y_arc = Rt + Rn * (1 - np.cos(theta_arc)) + + # Generate parabolic section (from N to E) + # Using quadratic Bezier curve approximation + n_parabola = num_points - n_arc + + # Control point for quadratic Bezier + # The tangent at N has slope tan(theta_n) + # The tangent at E has slope tan(theta_e) + # Find intersection of these tangents + + m_N = math.tan(theta_n_rad) + m_E = math.tan(theta_e_rad) + + # Line from N: y - y_N = m_N * (x - x_N) + # Line from E: y - y_E = m_E * (x - x_E) + # Solve for intersection (control point Q) + + if abs(m_N - m_E) > 1e-10: + x_Q = (y_E - y_N + m_N * x_N - m_E * x_E) / (m_N - m_E) + y_Q = y_N + m_N * (x_Q - x_N) + else: + # Parallel tangents (shouldn't happen for reasonable parameters) + x_Q = (x_N + x_E) / 2 + y_Q = (y_N + y_E) / 2 + + # Generate Bezier curve points + t = np.linspace(0, 1, n_parabola) + x_parabola = (1 - t) ** 2 * x_N + 2 * (1 - t) * t * x_Q + t**2 * x_E + y_parabola = (1 - t) ** 2 * y_N + 2 * (1 - t) * t * y_Q + t**2 * y_E + + # Combine arc and parabola (skip first point of parabola to avoid duplicate) + x = np.concatenate([x_arc, x_parabola[1:]]) + y = np.concatenate([y_arc, y_parabola[1:]]) + + return NozzleContour(x=x, y=y, contour_type="rao_bell") + + +def _rao_initial_angle(expansion_ratio: float, bell_fraction: float) -> float: + """Calculate initial expansion angle for Rao bell nozzle. + + Empirical correlation based on Rao's curves. + + Args: + expansion_ratio: Area ratio Ae/At + bell_fraction: Bell length fraction (0.6-1.0) + + Returns: + Initial angle in degrees + """ + # Empirical fit (approximation of Rao curves) + # For 80% bell: ~30-35° for low eps, ~20-25° for high eps + eps = expansion_ratio + + if bell_fraction <= 0.6: + theta = 38 - 2.5 * math.log10(eps) + elif bell_fraction <= 0.8: + theta = 33 - 3.5 * math.log10(eps) + else: + theta = 28 - 4.0 * math.log10(eps) + + # Clamp to reasonable values + return max(15.0, min(45.0, theta)) + + +def _rao_exit_angle(expansion_ratio: float, bell_fraction: float) -> float: + """Calculate exit angle for Rao bell nozzle. + + Empirical correlation based on Rao's curves. + + Args: + expansion_ratio: Area ratio Ae/At + bell_fraction: Bell length fraction (0.6-1.0) + + Returns: + Exit angle in degrees + """ + # Empirical fit + # Exit angle is typically 6-12° for most practical nozzles + eps = expansion_ratio + + if bell_fraction <= 0.6: + theta = 14 - 1.5 * math.log10(eps) + elif bell_fraction <= 0.8: + theta = 11 - 2.0 * math.log10(eps) + else: + theta = 8 - 2.5 * math.log10(eps) + + # Clamp to reasonable values + return max(4.0, min(15.0, theta)) + + +# ============================================================================= +# Conical Nozzle Contour +# ============================================================================= + + +@beartype +def conical_contour( + throat_radius: Quantity, + exit_radius: Quantity, + half_angle: float = 15.0, + num_points: int = 100, +) -> NozzleContour: + """Generate a conical nozzle contour. + + A simple conical nozzle with constant divergence angle. + The standard half-angle is 15°. + + Args: + throat_radius: Throat radius [length] + exit_radius: Exit radius [length] + half_angle: Nozzle half-angle in degrees (default 15°) + num_points: Number of points in the contour + + Returns: + NozzleContour with the conical shape + """ + Rt = throat_radius.to("m").value + Re = exit_radius.to("m").value + + # Nozzle length + Ln = (Re - Rt) / math.tan(math.radians(half_angle)) + + # Generate linear contour + x = np.linspace(0, Ln, num_points) + y = Rt + x * math.tan(math.radians(half_angle)) + + return NozzleContour(x=x, y=y, contour_type="conical") + + +# ============================================================================= +# Full Chamber Contour (Chamber + Convergent + Nozzle) +# ============================================================================= + + +@beartype +def full_chamber_contour( + inputs: EngineInputs, + geometry: EngineGeometry, + nozzle_contour: NozzleContour, + num_chamber_points: int = 50, + num_convergent_points: int = 30, +) -> NozzleContour: + """Generate complete chamber contour including chamber and convergent section. + + Combines: + 1. Cylindrical chamber section + 2. Convergent section (circular arc transition + conical) + 3. Throat region with circular arc + 4. Divergent nozzle section + + Args: + inputs: Engine inputs (for contraction angle) + geometry: Computed geometry + nozzle_contour: Pre-computed divergent nozzle contour + num_chamber_points: Points for cylindrical section + num_convergent_points: Points for convergent section + + Returns: + Complete chamber contour from inlet to exit + """ + # Extract dimensions + Rc = geometry.chamber_diameter.to("m").value / 2 + Rt = geometry.throat_diameter.to("m").value / 2 + Lcyl = geometry.chamber_length.to("m").value + contraction_angle = math.radians(inputs.contraction_angle) + + # Upstream radius of curvature (typically 1.5 * Rt for smooth transition) + R1 = 1.5 * Rt + + # Convergent section geometry + # The convergent has a circular arc transition from chamber to conical section + + # Calculate convergent section + # Point where circular arc meets conical section + theta_c = contraction_angle + x_tan = R1 * math.sin(theta_c) # axial distance from throat to tangent point + y_tan = Rt + R1 * (1 - math.cos(theta_c)) # radius at tangent point + + # Length of conical section (from tangent point to chamber) + L_cone = (Rc - y_tan) / math.tan(theta_c) if Rc > y_tan else 0 + + # Total convergent length + L_conv = x_tan + L_cone + + # Generate chamber section (negative x, before throat) + x_chamber = np.linspace(-(Lcyl + L_conv), -L_conv, num_chamber_points) + y_chamber = np.full_like(x_chamber, Rc) + + # Generate convergent conical section + if L_cone > 0: + n_cone = num_convergent_points // 2 + x_cone = np.linspace(-L_conv, -(x_tan), n_cone) + y_cone = Rc - (x_cone + L_conv) * math.tan(theta_c) + else: + x_cone = np.array([]) + y_cone = np.array([]) + + # Generate convergent circular arc (transition to throat) + n_arc = num_convergent_points - len(x_cone) + theta_range = np.linspace(math.pi - theta_c, math.pi, n_arc) + x_arc = R1 * np.cos(theta_range) # Goes from negative to 0 + y_arc = Rt + R1 + R1 * np.sin(theta_range) # Connects to throat + + # Shift nozzle contour (it starts at x=0 at throat) + x_nozzle = nozzle_contour.x + y_nozzle = nozzle_contour.y + + # Combine all sections + x_all = np.concatenate([x_chamber, x_cone, x_arc[:-1], x_nozzle]) + y_all = np.concatenate([y_chamber, y_cone, y_arc[:-1], y_nozzle]) + + return NozzleContour(x=x_all, y=y_all, contour_type=f"full_{nozzle_contour.contour_type}") + + +# ============================================================================= +# Convenience Functions +# ============================================================================= + + +@beartype +def generate_nozzle_from_geometry( + geometry: EngineGeometry, + bell_fraction: float = 0.8, + num_points: int = 100, +) -> NozzleContour: + """Generate a Rao bell nozzle contour from engine geometry. + + Convenience function that extracts the necessary parameters from + EngineGeometry. + + Args: + geometry: Computed engine geometry + bell_fraction: Bell length fraction (default 0.8) + num_points: Number of contour points + + Returns: + NozzleContour for the divergent section + """ + return rao_bell_contour( + throat_radius=geometry.throat_diameter / 2, + exit_radius=geometry.exit_diameter / 2, + expansion_ratio=geometry.expansion_ratio, + bell_fraction=bell_fraction, + num_points=num_points, + ) + diff --git a/openrocketengine/plotting.py b/openrocketengine/plotting.py new file mode 100644 index 0000000..8a9a7bc --- /dev/null +++ b/openrocketengine/plotting.py @@ -0,0 +1,659 @@ +"""Visualization module for OpenRocketEngine. + +Provides plotting functions for: +- Engine cross-section views +- Performance curves (Isp vs altitude, thrust vs altitude) +- Nozzle contour visualization +- Trade study plots + +All plots use matplotlib with a consistent, professional style. +""" + +import matplotlib.pyplot as plt +import numpy as np +from beartype import beartype +from matplotlib.figure import Figure +from matplotlib.patches import PathPatch +from matplotlib.path import Path as MplPath +from numpy.typing import NDArray + +from openrocketengine.engine import ( + EngineGeometry, + EngineInputs, + EnginePerformance, + isp_at_altitude, + thrust_at_altitude, +) +from openrocketengine.nozzle import NozzleContour +from openrocketengine.units import pascals + +# ============================================================================= +# Plot Style Configuration +# ============================================================================= + +# Professional color palette +COLORS = { + "primary": "#2E86AB", # Steel blue + "secondary": "#A23B72", # Berry + "accent": "#F18F01", # Orange + "chamber": "#454545", # Dark gray for chamber walls + "fill": "#E8E8E8", # Light gray for fill + "grid": "#CCCCCC", # Grid lines + "text": "#333333", # Text color +} + +# Default figure size +DEFAULT_FIGSIZE = (12, 6) + + +def _setup_style() -> None: + """Configure matplotlib style for consistent appearance.""" + plt.rcParams.update( + { + "font.family": "sans-serif", + "font.sans-serif": ["Helvetica", "Arial", "DejaVu Sans"], + "font.size": 11, + "axes.titlesize": 14, + "axes.labelsize": 12, + "axes.linewidth": 1.2, + "axes.edgecolor": COLORS["text"], + "axes.labelcolor": COLORS["text"], + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "xtick.color": COLORS["text"], + "ytick.color": COLORS["text"], + "legend.fontsize": 10, + "figure.titlesize": 16, + "grid.alpha": 0.5, + "grid.linewidth": 0.8, + } + ) + + +# ============================================================================= +# Engine Cross-Section Plot +# ============================================================================= + + +@beartype +def plot_engine_cross_section( + geometry: EngineGeometry, + contour: NozzleContour, + inputs: EngineInputs | None = None, + show_dimensions: bool = True, + show_centerline: bool = True, + figsize: tuple[float, float] = DEFAULT_FIGSIZE, + title: str | None = None, +) -> Figure: + """Plot a 2D cross-section of the engine chamber and nozzle. + + Creates a symmetric cross-section view showing: + - Chamber wall profile (top and bottom halves) + - Throat location + - Key dimensions (optional) + - Centerline (optional) + + Args: + geometry: Computed engine geometry + contour: Nozzle contour (can be just nozzle or full chamber) + inputs: Engine inputs (for title and additional info) + show_dimensions: Whether to annotate key dimensions + show_centerline: Whether to show the centerline + figsize: Figure size (width, height) in inches + title: Optional custom title + + Returns: + matplotlib Figure object + """ + _setup_style() + + fig, ax = plt.subplots(figsize=figsize) + + # Get contour data + x = contour.x + y = contour.y + + # Create symmetric contour (top and bottom) + x_full = np.concatenate([x, x[::-1]]) + y_full = np.concatenate([y, -y[::-1]]) + + # Create filled polygon for chamber wall + vertices = np.column_stack([x_full, y_full]) + codes = [MplPath.MOVETO] + [MplPath.LINETO] * (len(vertices) - 2) + [MplPath.CLOSEPOLY] + path = MplPath(vertices, codes) + patch = PathPatch( + path, + facecolor=COLORS["fill"], + edgecolor=COLORS["chamber"], + linewidth=2, + ) + ax.add_patch(patch) + + # Plot contour lines explicitly for clarity + ax.plot(x, y, color=COLORS["chamber"], linewidth=2, label="Chamber wall") + ax.plot(x, -y, color=COLORS["chamber"], linewidth=2) + + # Centerline + if show_centerline: + ax.axhline(y=0, color=COLORS["primary"], linestyle="--", linewidth=1, alpha=0.7) + ax.text( + x[-1] * 0.98, + 0, + "CL", + ha="right", + va="bottom", + fontsize=9, + color=COLORS["primary"], + ) + + # Mark throat location (minimum radius point) + throat_idx = np.argmin(y) + throat_x = x[throat_idx] + throat_y = y[throat_idx] + + ax.axvline(x=throat_x, color=COLORS["secondary"], linestyle=":", linewidth=1.5, alpha=0.7) + ax.plot(throat_x, throat_y, "o", color=COLORS["secondary"], markersize=6) + ax.plot(throat_x, -throat_y, "o", color=COLORS["secondary"], markersize=6) + + # Dimension annotations + if show_dimensions: + _add_dimension_annotations(ax, geometry, contour, x, y) + + # Set axis properties + ax.set_aspect("equal") + ax.set_xlabel("Axial Position (m)") + ax.set_ylabel("Radial Position (m)") + + # Add margin + x_margin = (x[-1] - x[0]) * 0.1 + y_max = max(y) * 1.3 + ax.set_xlim(x[0] - x_margin, x[-1] + x_margin) + ax.set_ylim(-y_max, y_max) + + # Grid + ax.grid(True, alpha=0.3, linestyle="-", linewidth=0.5) + + # Title + if title: + ax.set_title(title) + elif inputs and inputs.name: + ax.set_title(f"Engine Cross-Section: {inputs.name}") + else: + ax.set_title("Engine Cross-Section") + + fig.tight_layout() + return fig + + +def _add_dimension_annotations( + ax: plt.Axes, + geometry: EngineGeometry, + contour: NozzleContour, + x: NDArray[np.float64], + y: NDArray[np.float64], +) -> None: + """Add dimension annotations to the cross-section plot.""" + # Throat diameter + throat_idx = np.argmin(y) + throat_x = x[throat_idx] + throat_r = y[throat_idx] + + # Exit diameter + exit_r = y[-1] + exit_x = x[-1] + + # Annotation style + arrowprops = dict(arrowstyle="<->", color=COLORS["accent"], lw=1.5) + text_offset = 0.02 * (x[-1] - x[0]) + + # Throat diameter annotation + Dt_mm = geometry.throat_diameter.to("m").value * 1000 + ax.annotate( + "", + xy=(throat_x, throat_r), + xytext=(throat_x, -throat_r), + arrowprops=arrowprops, + ) + ax.text( + throat_x + text_offset, + 0, + f"Dt={Dt_mm:.1f}mm", + fontsize=9, + va="center", + color=COLORS["accent"], + ) + + # Exit diameter annotation + De_mm = geometry.exit_diameter.to("m").value * 1000 + ax.annotate( + "", + xy=(exit_x, exit_r), + xytext=(exit_x, -exit_r), + arrowprops=arrowprops, + ) + ax.text( + exit_x - text_offset, + exit_r * 0.5, + f"De={De_mm:.1f}mm", + fontsize=9, + va="center", + ha="right", + color=COLORS["accent"], + ) + + # Expansion ratio annotation + eps = geometry.expansion_ratio + ax.text( + exit_x - text_offset, + -exit_r * 0.5, + f"ε={eps:.1f}", + fontsize=9, + va="center", + ha="right", + color=COLORS["text"], + ) + + +# ============================================================================= +# Nozzle Contour Plot +# ============================================================================= + + +@beartype +def plot_nozzle_contour( + contour: NozzleContour, + figsize: tuple[float, float] = (10, 6), + title: str | None = None, + units: str = "mm", +) -> Figure: + """Plot a nozzle contour profile. + + Shows just the nozzle contour (single line, not symmetric view). + Useful for verifying contour generation and CAD export. + + Args: + contour: Nozzle contour to plot + figsize: Figure size + title: Optional title + units: Display units ("m" or "mm") + + Returns: + matplotlib Figure + """ + _setup_style() + + fig, ax = plt.subplots(figsize=figsize) + + if units == "mm": + x = contour.x * 1000 + y = contour.y * 1000 + xlabel = "Axial Position (mm)" + ylabel = "Radius (mm)" + else: + x = contour.x + y = contour.y + xlabel = "Axial Position (m)" + ylabel = "Radius (m)" + + ax.plot(x, y, color=COLORS["primary"], linewidth=2, label="Contour") + ax.fill_between(x, 0, y, alpha=0.2, color=COLORS["primary"]) + + # Mark throat + throat_idx = np.argmin(y) + ax.axvline(x=x[throat_idx], color=COLORS["secondary"], linestyle=":", alpha=0.7) + ax.plot(x[throat_idx], y[throat_idx], "o", color=COLORS["secondary"], markersize=8) + ax.text( + x[throat_idx], + y[throat_idx] * 1.1, + "Throat", + ha="center", + fontsize=10, + color=COLORS["secondary"], + ) + + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.set_title(title or f"Nozzle Contour ({contour.contour_type})") + ax.grid(True, alpha=0.3) + ax.set_ylim(bottom=0) + + fig.tight_layout() + return fig + + +# ============================================================================= +# Performance vs Altitude +# ============================================================================= + + +@beartype +def plot_performance_vs_altitude( + inputs: EngineInputs, + performance: EnginePerformance, + geometry: EngineGeometry, + max_altitude_km: float = 100.0, + num_points: int = 100, + figsize: tuple[float, float] = DEFAULT_FIGSIZE, +) -> Figure: + """Plot thrust and Isp vs altitude. + + Shows how engine performance changes with altitude due to + decreasing ambient pressure. + + Args: + inputs: Engine inputs + performance: Computed performance + geometry: Computed geometry + max_altitude_km: Maximum altitude to plot (km) + num_points: Number of altitude points + figsize: Figure size + + Returns: + matplotlib Figure with two subplots + """ + _setup_style() + + # Generate altitude array + altitudes_km = np.linspace(0, max_altitude_km, num_points) + + # Simple exponential atmosphere model + # P = P0 * exp(-h/H) where H ≈ 8.5 km + P0 = 101325 # Pa + H = 8500 # m + pressures_Pa = P0 * np.exp(-altitudes_km * 1000 / H) + + # Calculate thrust and Isp at each altitude + thrust_vals = np.zeros(num_points) + isp_vals = np.zeros(num_points) + + for i, pa in enumerate(pressures_Pa): + pa_qty = pascals(pa) + thrust_vals[i] = thrust_at_altitude(inputs, performance, geometry, pa_qty).to("kN").value + isp_vals[i] = isp_at_altitude(inputs, performance, pa_qty).value + + # Create figure with two subplots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) + + # Thrust plot + ax1.plot(altitudes_km, thrust_vals, color=COLORS["primary"], linewidth=2) + ax1.axhline( + y=inputs.thrust.to("kN").value, + color=COLORS["secondary"], + linestyle="--", + alpha=0.7, + label="Design thrust (SL)", + ) + ax1.set_xlabel("Altitude (km)") + ax1.set_ylabel("Thrust (kN)") + ax1.set_title("Thrust vs Altitude") + ax1.grid(True, alpha=0.3) + ax1.legend() + ax1.set_xlim(0, max_altitude_km) + + # Isp plot + ax2.plot(altitudes_km, isp_vals, color=COLORS["accent"], linewidth=2) + ax2.axhline( + y=performance.isp.value, + color=COLORS["secondary"], + linestyle="--", + alpha=0.7, + label="Isp (SL)", + ) + ax2.axhline( + y=performance.isp_vac.value, + color=COLORS["primary"], + linestyle=":", + alpha=0.7, + label="Isp (Vac)", + ) + ax2.set_xlabel("Altitude (km)") + ax2.set_ylabel("Specific Impulse (s)") + ax2.set_title("Isp vs Altitude") + ax2.grid(True, alpha=0.3) + ax2.legend() + ax2.set_xlim(0, max_altitude_km) + + # Overall title + name = inputs.name or "Engine" + fig.suptitle(f"Altitude Performance: {name}", fontsize=14, y=1.02) + + fig.tight_layout() + return fig + + +# ============================================================================= +# Trade Study Plots +# ============================================================================= + + +@beartype +def plot_isp_vs_expansion_ratio( + gamma: float = 1.2, + pc_pe_range: tuple[float, float] = (10, 200), + num_points: int = 100, + figsize: tuple[float, float] = (10, 6), +) -> Figure: + """Plot theoretical Isp vs expansion ratio for different pressure ratios. + + Useful for understanding nozzle design trade-offs. + + Args: + gamma: Ratio of specific heats + pc_pe_range: Range of chamber-to-exit pressure ratios + num_points: Number of points + figsize: Figure size + + Returns: + matplotlib Figure + """ + from openrocketengine.isentropic import ( + area_ratio_from_mach, + mach_from_pressure_ratio, + thrust_coefficient, + ) + + _setup_style() + + fig, ax = plt.subplots(figsize=figsize) + + # Different pressure ratios to plot + pc_pe_values = [20, 50, 100, 150, 200] + + for pc_pe in pc_pe_values: + # Calculate exit Mach + Me = mach_from_pressure_ratio(pc_pe, gamma) + eps = area_ratio_from_mach(Me, gamma) + + # Calculate Cf for perfectly expanded nozzle (pa = pe) + pe_pc = 1.0 / pc_pe + Cf = thrust_coefficient(gamma, pe_pc, pe_pc, eps) + + # Plot point + ax.scatter([eps], [Cf], s=100, zorder=5) + ax.annotate( + f"pc/pe={pc_pe}", + xy=(eps, Cf), + xytext=(10, 5), + textcoords="offset points", + fontsize=9, + ) + + # Generate curve for range of expansion ratios + eps_range = np.linspace(2, 100, 200) + Cf_optimal = np.zeros_like(eps_range) + + for i, eps in enumerate(eps_range): + # Find pressure ratio that gives this expansion ratio + Me = 2.0 # Initial guess + for _ in range(50): + eps_calc = area_ratio_from_mach(Me, gamma) + if abs(eps_calc - eps) < 0.01: + break + Me += (eps - eps_calc) * 0.1 + + # Cf for this expansion ratio (optimally expanded) + pc_pe = (1 + (gamma - 1) / 2 * Me**2) ** (gamma / (gamma - 1)) + pe_pc = 1.0 / pc_pe + Cf_optimal[i] = thrust_coefficient(gamma, pe_pc, pe_pc, eps) + + ax.plot(eps_range, Cf_optimal, color=COLORS["primary"], linewidth=2, label="Optimal Cf") + + ax.set_xlabel("Expansion Ratio (Ae/At)") + ax.set_ylabel("Thrust Coefficient (Cf)") + ax.set_title(f"Thrust Coefficient vs Expansion Ratio (γ={gamma})") + ax.grid(True, alpha=0.3) + ax.legend() + + fig.tight_layout() + return fig + + +# ============================================================================= +# Summary Dashboard +# ============================================================================= + + +@beartype +def plot_engine_dashboard( + inputs: EngineInputs, + performance: EnginePerformance, + geometry: EngineGeometry, + contour: NozzleContour, + figsize: tuple[float, float] = (16, 10), +) -> Figure: + """Create a comprehensive dashboard with engine summary. + + Includes: + - Engine cross-section + - Performance vs altitude + - Key parameters table + + Args: + inputs: Engine inputs + performance: Computed performance + geometry: Computed geometry + contour: Nozzle contour + figsize: Figure size + + Returns: + matplotlib Figure + """ + _setup_style() + + fig = plt.figure(figsize=figsize) + + # Create grid layout + gs = fig.add_gridspec(2, 3, height_ratios=[1.2, 1], width_ratios=[1.5, 1, 1]) + + # Cross-section (spans two columns) + ax_cross = fig.add_subplot(gs[0, :2]) + + # Get contour data + x = contour.x + y = contour.y + + # Plot symmetric contour + ax_cross.fill_between(x, y, -y, color=COLORS["fill"], alpha=0.8) + ax_cross.plot(x, y, color=COLORS["chamber"], linewidth=2) + ax_cross.plot(x, -y, color=COLORS["chamber"], linewidth=2) + ax_cross.axhline(y=0, color=COLORS["primary"], linestyle="--", linewidth=1, alpha=0.5) + + # Mark throat + throat_idx = np.argmin(y) + ax_cross.axvline(x=x[throat_idx], color=COLORS["secondary"], linestyle=":", alpha=0.7) + + ax_cross.set_aspect("equal") + ax_cross.set_xlabel("Axial Position (m)") + ax_cross.set_ylabel("Radial Position (m)") + ax_cross.set_title("Engine Cross-Section") + ax_cross.grid(True, alpha=0.3) + + # Parameters table (right side of top row) + ax_params = fig.add_subplot(gs[0, 2]) + ax_params.axis("off") + + # Create parameter text + name = inputs.name or "Unnamed Engine" + params_text = f""" + {name} + ───────────────────── + PERFORMANCE + Thrust (SL): {inputs.thrust.to('kN').value:.2f} kN + Isp (SL): {performance.isp.value:.1f} s + Isp (Vac): {performance.isp_vac.value:.1f} s + C*: {performance.cstar.value:.0f} m/s + + MASS FLOW + Total: {performance.mdot.value:.3f} kg/s + O/F Ratio: {inputs.mixture_ratio:.2f} + + GEOMETRY + Dt: {geometry.throat_diameter.to('m').value*1000:.1f} mm + De: {geometry.exit_diameter.to('m').value*1000:.1f} mm + ε (Ae/At): {geometry.expansion_ratio:.1f} + + CONDITIONS + Pc: {inputs.chamber_pressure.to('MPa').value:.2f} MPa + Tc: {inputs.chamber_temp.to('K').value:.0f} K + γ: {inputs.gamma:.3f} + """ + + ax_params.text( + 0.1, + 0.95, + params_text, + transform=ax_params.transAxes, + fontsize=10, + fontfamily="monospace", + verticalalignment="top", + bbox=dict(boxstyle="round", facecolor="white", edgecolor=COLORS["grid"]), + ) + + # Altitude performance plots (bottom row) + altitudes_km = np.linspace(0, 80, 50) + P0 = 101325 + H = 8500 + pressures_Pa = P0 * np.exp(-altitudes_km * 1000 / H) + + thrust_vals = np.zeros(len(altitudes_km)) + isp_vals = np.zeros(len(altitudes_km)) + + for i, pa in enumerate(pressures_Pa): + pa_qty = pascals(pa) + thrust_vals[i] = thrust_at_altitude(inputs, performance, geometry, pa_qty).to("kN").value + isp_vals[i] = isp_at_altitude(inputs, performance, pa_qty).value + + # Thrust vs altitude + ax_thrust = fig.add_subplot(gs[1, 0]) + ax_thrust.plot(altitudes_km, thrust_vals, color=COLORS["primary"], linewidth=2) + ax_thrust.set_xlabel("Altitude (km)") + ax_thrust.set_ylabel("Thrust (kN)") + ax_thrust.set_title("Thrust vs Altitude") + ax_thrust.grid(True, alpha=0.3) + + # Isp vs altitude + ax_isp = fig.add_subplot(gs[1, 1]) + ax_isp.plot(altitudes_km, isp_vals, color=COLORS["accent"], linewidth=2) + ax_isp.axhline(y=performance.isp_vac.value, color=COLORS["secondary"], linestyle="--", alpha=0.7) + ax_isp.set_xlabel("Altitude (km)") + ax_isp.set_ylabel("Isp (s)") + ax_isp.set_title("Specific Impulse vs Altitude") + ax_isp.grid(True, alpha=0.3) + + # Nozzle contour detail + ax_nozzle = fig.add_subplot(gs[1, 2]) + x_mm = contour.x * 1000 + y_mm = contour.y * 1000 + ax_nozzle.plot(x_mm, y_mm, color=COLORS["primary"], linewidth=2) + ax_nozzle.fill_between(x_mm, 0, y_mm, alpha=0.2, color=COLORS["primary"]) + ax_nozzle.set_xlabel("x (mm)") + ax_nozzle.set_ylabel("r (mm)") + ax_nozzle.set_title(f"Nozzle Contour ({contour.contour_type})") + ax_nozzle.grid(True, alpha=0.3) + ax_nozzle.set_ylim(bottom=0) + + fig.suptitle(f"Engine Design Summary: {name}", fontsize=16, fontweight="bold", y=0.98) + fig.tight_layout(rect=[0, 0, 1, 0.96]) + + return fig + diff --git a/openrocketengine/units.py b/openrocketengine/units.py new file mode 100644 index 0000000..c039bf0 --- /dev/null +++ b/openrocketengine/units.py @@ -0,0 +1,622 @@ +"""Units module for OpenRocketEngine. + +Provides a Quantity class for type-safe physical quantities with unit conversion. +All physical values in the library should use Quantity, never bare floats. + +Design principles: +- Explicit over implicit: all conversions require calling .to() +- Type safe: beartype checks at runtime +- Immutable: frozen dataclasses prevent accidental mutation +- No magic: clear, predictable behavior +""" + +import math +from dataclasses import dataclass + +from beartype import beartype + +# ============================================================================= +# Dimension and Unit Definitions +# ============================================================================= + +# Dimensions represent physical quantities (length, mass, time, etc.) +# Each dimension has a base SI unit + +DIMENSIONS = { + "length": "m", + "mass": "kg", + "time": "s", + "temperature": "K", + "force": "N", + "pressure": "Pa", + "velocity": "m/s", + "area": "m^2", + "volume": "m^3", + "mass_flow": "kg/s", + "density": "kg/m^3", + "specific_impulse": "s", + "dimensionless": "1", +} + +# Conversion factors TO base SI unit +# e.g., 1 ft = 0.3048 m, so CONVERSIONS["ft"] = 0.3048 +CONVERSIONS: dict[str, tuple[float, str]] = { + # Length + "m": (1.0, "length"), + "cm": (0.01, "length"), + "mm": (0.001, "length"), + "km": (1000.0, "length"), + "ft": (0.3048, "length"), + "in": (0.0254, "length"), + "inch": (0.0254, "length"), + "inches": (0.0254, "length"), + # Mass + "kg": (1.0, "mass"), + "g": (0.001, "mass"), + "lbm": (0.453592, "mass"), + "slug": (14.5939, "mass"), + # Time + "s": (1.0, "time"), + "ms": (0.001, "time"), + "min": (60.0, "time"), + "hr": (3600.0, "time"), + # Temperature (special - requires offset handling) + "K": (1.0, "temperature"), + "R": (5 / 9, "temperature"), # Rankine to Kelvin (multiply only, offset handled separately) + # Force + "N": (1.0, "force"), + "kN": (1000.0, "force"), + "MN": (1e6, "force"), + "lbf": (4.44822, "force"), + "kgf": (9.80665, "force"), + # Pressure + "Pa": (1.0, "pressure"), + "kPa": (1000.0, "pressure"), + "MPa": (1e6, "pressure"), + "bar": (1e5, "pressure"), + "atm": (101325.0, "pressure"), + "psi": (6894.76, "pressure"), + "psia": (6894.76, "pressure"), + # Velocity + "m/s": (1.0, "velocity"), + "km/s": (1000.0, "velocity"), + "ft/s": (0.3048, "velocity"), + # Area + "m^2": (1.0, "area"), + "cm^2": (1e-4, "area"), + "mm^2": (1e-6, "area"), + "ft^2": (0.092903, "area"), + "in^2": (0.00064516, "area"), + # Volume + "m^3": (1.0, "volume"), + "L": (0.001, "volume"), + "cm^3": (1e-6, "volume"), + "ft^3": (0.0283168, "volume"), + "in^3": (1.6387e-5, "volume"), + # Mass flow rate + "kg/s": (1.0, "mass_flow"), + "lbm/s": (0.453592, "mass_flow"), + # Density + "kg/m^3": (1.0, "density"), + "lbm/ft^3": (16.0185, "density"), + # Specific impulse (time dimension but special meaning) + # Note: Isp in seconds is the same in SI and Imperial + # Dimensionless + "1": (1.0, "dimensionless"), + "": (1.0, "dimensionless"), +} + + +def _get_dimension(unit: str) -> str: + """Get the dimension for a unit string.""" + if unit not in CONVERSIONS: + raise ValueError(f"Unknown unit: {unit!r}") + return CONVERSIONS[unit][1] + + +def _get_conversion_factor(unit: str) -> float: + """Get the conversion factor to SI base unit.""" + if unit not in CONVERSIONS: + raise ValueError(f"Unknown unit: {unit!r}") + return CONVERSIONS[unit][0] + + +def _convert(value: float, from_unit: str, to_unit: str) -> float: + """Convert a value between units of the same dimension.""" + from_dim = _get_dimension(from_unit) + to_dim = _get_dimension(to_unit) + + if from_dim != to_dim: + raise ValueError( + f"Cannot convert between different dimensions: {from_dim} and {to_dim}" + ) + + # Convert to SI base, then to target + si_value = value * _get_conversion_factor(from_unit) + return si_value / _get_conversion_factor(to_unit) + + +# ============================================================================= +# Quantity Class +# ============================================================================= + + +@beartype +@dataclass(frozen=True, slots=True) +class Quantity: + """A physical quantity with value, unit, and dimension. + + Quantities are immutable and support arithmetic operations that respect + dimensional analysis. + + Examples: + >>> thrust = Quantity(50000, "N", "force") + >>> thrust_lbf = thrust.to("lbf") + >>> print(thrust_lbf) + Quantity(11240.45 lbf) + + >>> length = meters(2.5) + >>> area = length * length # Returns Quantity with area dimension + """ + + value: float | int + unit: str + dimension: str + + def __post_init__(self) -> None: + """Validate that unit matches dimension.""" + if self.unit not in CONVERSIONS: + raise ValueError(f"Unknown unit: {self.unit!r}") + expected_dim = CONVERSIONS[self.unit][1] + if self.dimension != expected_dim: + raise ValueError( + f"Unit {self.unit!r} has dimension {expected_dim!r}, " + f"but {self.dimension!r} was specified" + ) + + def to(self, target_unit: str) -> "Quantity": + """Convert to a different unit of the same dimension. + + Args: + target_unit: The unit to convert to + + Returns: + A new Quantity with the converted value and new unit + + Raises: + ValueError: If target_unit is incompatible dimension + """ + new_value = _convert(self.value, self.unit, target_unit) + return Quantity(new_value, target_unit, self.dimension) + + def to_si(self) -> "Quantity": + """Convert to SI base unit for this dimension.""" + si_unit = DIMENSIONS[self.dimension] + return self.to(si_unit) + + @property + def si_value(self) -> float: + """Get the value in SI base units without creating new Quantity.""" + return self.value * _get_conversion_factor(self.unit) + + def __repr__(self) -> str: + return f"Quantity({self.value:.6g} {self.unit})" + + def __str__(self) -> str: + return f"{self.value:.6g} {self.unit}" + + # ------------------------------------------------------------------------- + # Arithmetic Operations + # ------------------------------------------------------------------------- + + def __add__(self, other: "Quantity") -> "Quantity": + """Add two quantities of the same dimension.""" + if not isinstance(other, Quantity): + raise TypeError(f"Cannot add Quantity and {type(other).__name__}") + if self.dimension != other.dimension: + raise ValueError( + f"Cannot add quantities with different dimensions: " + f"{self.dimension} and {other.dimension}" + ) + # Convert other to same unit as self, then add + other_converted = other.to(self.unit) + return Quantity(self.value + other_converted.value, self.unit, self.dimension) + + def __sub__(self, other: "Quantity") -> "Quantity": + """Subtract two quantities of the same dimension.""" + if not isinstance(other, Quantity): + raise TypeError(f"Cannot subtract Quantity and {type(other).__name__}") + if self.dimension != other.dimension: + raise ValueError( + f"Cannot subtract quantities with different dimensions: " + f"{self.dimension} and {other.dimension}" + ) + other_converted = other.to(self.unit) + return Quantity(self.value - other_converted.value, self.unit, self.dimension) + + def __mul__(self, other: "Quantity | float | int") -> "Quantity": + """Multiply by a scalar or another quantity.""" + if isinstance(other, (int, float)): + return Quantity(self.value * other, self.unit, self.dimension) + if isinstance(other, Quantity): + # Dimensional multiplication - result dimension depends on operands + new_dim, new_unit = _multiply_dimensions( + self.dimension, self.unit, other.dimension, other.unit + ) + new_value = self.si_value * other.si_value + # Convert back from SI to the derived unit + new_value = new_value / _get_conversion_factor(new_unit) + return Quantity(new_value, new_unit, new_dim) + raise TypeError(f"Cannot multiply Quantity by {type(other).__name__}") + + def __rmul__(self, other: float | int) -> "Quantity": + """Right multiply by scalar.""" + if isinstance(other, (int, float)): + return Quantity(self.value * other, self.unit, self.dimension) + raise TypeError(f"Cannot multiply {type(other).__name__} by Quantity") + + def __truediv__(self, other: "Quantity | float | int") -> "Quantity": + """Divide by a scalar or another quantity.""" + if isinstance(other, (int, float)): + return Quantity(self.value / other, self.unit, self.dimension) + if isinstance(other, Quantity): + new_dim, new_unit = _divide_dimensions( + self.dimension, self.unit, other.dimension, other.unit + ) + new_value = self.si_value / other.si_value + new_value = new_value / _get_conversion_factor(new_unit) + return Quantity(new_value, new_unit, new_dim) + raise TypeError(f"Cannot divide Quantity by {type(other).__name__}") + + def __rtruediv__(self, other: float | int) -> "Quantity": + """Right division (scalar / Quantity) - returns inverse dimension.""" + if isinstance(other, (int, float)): + # This would create an inverse dimension which we don't fully support + # For now, raise an error + raise TypeError( + "Division of scalar by Quantity not supported. " + "Use explicit inverse units instead." + ) + raise TypeError(f"Cannot divide {type(other).__name__} by Quantity") + + def __neg__(self) -> "Quantity": + """Negate the quantity.""" + return Quantity(-self.value, self.unit, self.dimension) + + def __pos__(self) -> "Quantity": + """Positive (returns copy).""" + return Quantity(self.value, self.unit, self.dimension) + + def __abs__(self) -> "Quantity": + """Absolute value.""" + return Quantity(abs(self.value), self.unit, self.dimension) + + # ------------------------------------------------------------------------- + # Comparison Operations + # ------------------------------------------------------------------------- + + def __eq__(self, other: object) -> bool: + """Check equality (compares SI values for same dimension).""" + if not isinstance(other, Quantity): + return NotImplemented + if self.dimension != other.dimension: + return False + # Compare in SI units to handle unit differences + return math.isclose(self.si_value, other.si_value, rel_tol=1e-9) + + def __lt__(self, other: "Quantity") -> bool: + if not isinstance(other, Quantity): + raise TypeError(f"Cannot compare Quantity with {type(other).__name__}") + if self.dimension != other.dimension: + raise ValueError("Cannot compare quantities with different dimensions") + return self.si_value < other.si_value + + def __le__(self, other: "Quantity") -> bool: + return self == other or self < other + + def __gt__(self, other: "Quantity") -> bool: + if not isinstance(other, Quantity): + raise TypeError(f"Cannot compare Quantity with {type(other).__name__}") + if self.dimension != other.dimension: + raise ValueError("Cannot compare quantities with different dimensions") + return self.si_value > other.si_value + + def __ge__(self, other: "Quantity") -> bool: + return self == other or self > other + + def __hash__(self) -> int: + """Hash based on SI value and dimension for consistency.""" + return hash((round(self.si_value, 9), self.dimension)) + + +# ============================================================================= +# Dimension Algebra +# ============================================================================= + +# Multiplication table for dimensions +_MULT_TABLE: dict[tuple[str, str], str] = { + ("length", "length"): "area", + ("area", "length"): "volume", + ("length", "area"): "volume", + ("velocity", "time"): "length", + ("mass", "velocity"): "force", # Approximation: momentum + ("force", "length"): "force", # Work/energy - simplified + ("pressure", "area"): "force", + ("mass_flow", "velocity"): "force", + ("mass_flow", "time"): "mass", + ("density", "volume"): "mass", + ("dimensionless", "length"): "length", + ("dimensionless", "mass"): "mass", + ("dimensionless", "force"): "force", + ("dimensionless", "pressure"): "pressure", + ("dimensionless", "velocity"): "velocity", + ("dimensionless", "area"): "area", + ("dimensionless", "volume"): "volume", + ("dimensionless", "time"): "time", + ("dimensionless", "temperature"): "temperature", + ("dimensionless", "mass_flow"): "mass_flow", + ("dimensionless", "density"): "density", + ("dimensionless", "dimensionless"): "dimensionless", +} + +# Division table for dimensions +_DIV_TABLE: dict[tuple[str, str], str] = { + ("area", "length"): "length", + ("volume", "length"): "area", + ("volume", "area"): "length", + ("length", "time"): "velocity", + ("velocity", "time"): "velocity", # acceleration - simplified + ("mass", "volume"): "density", + ("mass", "time"): "mass_flow", + ("force", "area"): "pressure", + ("force", "mass"): "velocity", # acceleration simplified + ("force", "pressure"): "area", + ("force", "velocity"): "mass_flow", + ("length", "length"): "dimensionless", + ("mass", "mass"): "dimensionless", + ("force", "force"): "dimensionless", + ("pressure", "pressure"): "dimensionless", + ("area", "area"): "dimensionless", + ("volume", "volume"): "dimensionless", + ("velocity", "velocity"): "dimensionless", + ("time", "time"): "dimensionless", + ("dimensionless", "dimensionless"): "dimensionless", +} + + +def _multiply_dimensions( + dim1: str, unit1: str, dim2: str, unit2: str +) -> tuple[str, str]: + """Determine result dimension and unit for multiplication.""" + # Check both orderings + key = (dim1, dim2) + if key in _MULT_TABLE: + result_dim = _MULT_TABLE[key] + elif (dim2, dim1) in _MULT_TABLE: + result_dim = _MULT_TABLE[(dim2, dim1)] + else: + raise ValueError( + f"Multiplication of {dim1} and {dim2} not supported. " + "Result dimension is ambiguous." + ) + + result_unit = DIMENSIONS[result_dim] + return result_dim, result_unit + + +def _divide_dimensions(dim1: str, unit1: str, dim2: str, unit2: str) -> tuple[str, str]: + """Determine result dimension and unit for division.""" + key = (dim1, dim2) + if key in _DIV_TABLE: + result_dim = _DIV_TABLE[key] + else: + raise ValueError( + f"Division of {dim1} by {dim2} not supported. " + "Result dimension is ambiguous." + ) + + result_unit = DIMENSIONS[result_dim] + return result_dim, result_unit + + +# ============================================================================= +# Factory Functions - Clear, Explicit Quantity Creation +# ============================================================================= + + +@beartype +def meters(value: float | int) -> Quantity: + """Create a length quantity in meters.""" + return Quantity(value, "m", "length") + + +@beartype +def centimeters(value: float | int) -> Quantity: + """Create a length quantity in centimeters.""" + return Quantity(value, "cm", "length") + + +@beartype +def millimeters(value: float | int) -> Quantity: + """Create a length quantity in millimeters.""" + return Quantity(value, "mm", "length") + + +@beartype +def feet(value: float | int) -> Quantity: + """Create a length quantity in feet.""" + return Quantity(value, "ft", "length") + + +@beartype +def inches(value: float | int) -> Quantity: + """Create a length quantity in inches.""" + return Quantity(value, "in", "length") + + +@beartype +def kilograms(value: float | int) -> Quantity: + """Create a mass quantity in kilograms.""" + return Quantity(value, "kg", "mass") + + +@beartype +def pounds_mass(value: float | int) -> Quantity: + """Create a mass quantity in pounds-mass.""" + return Quantity(value, "lbm", "mass") + + +@beartype +def seconds(value: float | int) -> Quantity: + """Create a time quantity in seconds.""" + return Quantity(value, "s", "time") + + +@beartype +def kelvin(value: float | int) -> Quantity: + """Create a temperature quantity in Kelvin.""" + return Quantity(value, "K", "temperature") + + +@beartype +def rankine(value: float | int) -> Quantity: + """Create a temperature quantity in Rankine.""" + return Quantity(value, "R", "temperature") + + +@beartype +def newtons(value: float | int) -> Quantity: + """Create a force quantity in Newtons.""" + return Quantity(value, "N", "force") + + +@beartype +def kilonewtons(value: float | int) -> Quantity: + """Create a force quantity in kilonewtons.""" + return Quantity(value, "kN", "force") + + +@beartype +def pounds_force(value: float | int) -> Quantity: + """Create a force quantity in pounds-force.""" + return Quantity(value, "lbf", "force") + + +@beartype +def pascals(value: float | int) -> Quantity: + """Create a pressure quantity in Pascals.""" + return Quantity(value, "Pa", "pressure") + + +@beartype +def kilopascals(value: float | int) -> Quantity: + """Create a pressure quantity in kilopascals.""" + return Quantity(value, "kPa", "pressure") + + +@beartype +def megapascals(value: float | int) -> Quantity: + """Create a pressure quantity in megapascals.""" + return Quantity(value, "MPa", "pressure") + + +@beartype +def bar(value: float | int) -> Quantity: + """Create a pressure quantity in bar.""" + return Quantity(value, "bar", "pressure") + + +@beartype +def atmospheres(value: float | int) -> Quantity: + """Create a pressure quantity in atmospheres.""" + return Quantity(value, "atm", "pressure") + + +@beartype +def psi(value: float | int) -> Quantity: + """Create a pressure quantity in psi.""" + return Quantity(value, "psi", "pressure") + + +@beartype +def meters_per_second(value: float | int) -> Quantity: + """Create a velocity quantity in m/s.""" + return Quantity(value, "m/s", "velocity") + + +@beartype +def feet_per_second(value: float | int) -> Quantity: + """Create a velocity quantity in ft/s.""" + return Quantity(value, "ft/s", "velocity") + + +@beartype +def square_meters(value: float | int) -> Quantity: + """Create an area quantity in m^2.""" + return Quantity(value, "m^2", "area") + + +@beartype +def square_centimeters(value: float | int) -> Quantity: + """Create an area quantity in cm^2.""" + return Quantity(value, "cm^2", "area") + + +@beartype +def square_inches(value: float | int) -> Quantity: + """Create an area quantity in in^2.""" + return Quantity(value, "in^2", "area") + + +@beartype +def cubic_meters(value: float | int) -> Quantity: + """Create a volume quantity in m^3.""" + return Quantity(value, "m^3", "volume") + + +@beartype +def liters(value: float | int) -> Quantity: + """Create a volume quantity in liters.""" + return Quantity(value, "L", "volume") + + +@beartype +def kg_per_second(value: float | int) -> Quantity: + """Create a mass flow rate quantity in kg/s.""" + return Quantity(value, "kg/s", "mass_flow") + + +@beartype +def lbm_per_second(value: float | int) -> Quantity: + """Create a mass flow rate quantity in lbm/s.""" + return Quantity(value, "lbm/s", "mass_flow") + + +@beartype +def kg_per_cubic_meter(value: float | int) -> Quantity: + """Create a density quantity in kg/m^3.""" + return Quantity(value, "kg/m^3", "density") + + +@beartype +def dimensionless(value: float | int) -> Quantity: + """Create a dimensionless quantity.""" + return Quantity(value, "1", "dimensionless") + + +# ============================================================================= +# Constants +# ============================================================================= + +# Standard gravity +G0_SI = meters_per_second(9.80665) +G0_IMP = feet_per_second(32.174) + +# Standard atmospheric pressure +ATM_SI = pascals(101325.0) +ATM_IMP = psi(14.696) + +# Universal gas constant +R_UNIVERSAL_SI = 8314.46 # J/(kmol·K) - stored as float, used in calculations +R_UNIVERSAL_IMP = 1545.35 # ft·lbf/(lbmol·R) + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f3da031 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[project] +name = "openrocketengine" +version = "0.2.0" +description = "OpenRocketEngine - Tools for liquid rocket engine design and analysis" +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +authors = [ + { name = "Cameron Flannery" } +] +keywords = ["rocket", "propulsion", "aerospace", "engineering", "nozzle"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Intended Audience :: Education", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Physics", +] + +dependencies = [ + "numpy>=2.0", + "beartype>=0.18", + "numba>=0.60", + "matplotlib>=3.9", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-cov>=4.0", + "ruff>=0.4", +] +cea = [ + "rocketcea>=1.2", +] + +[project.urls] +Homepage = "https://github.com/openrocketengine/openrocketengine" +Documentation = "https://openrocketengine.readthedocs.io" +Repository = "https://github.com/openrocketengine/openrocketengine" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-v --tb=short" + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B", "SIM"] +ignore = ["E501"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4e6b3c7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -numpy>=1.0 -pandas>=0.20 -fire>=0.1.3 -pytest -xlsxwriter diff --git a/setup.py b/setup.py deleted file mode 100644 index 7a192a1..0000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Setup module for openrocketengine.""" -from distutils.core import setup - - -setup( - name="openrocketengine", - version="0.0.1dev", - description="A collection of tools used for high level rocket engine design.", - url=["https://github.com/cmflannery/openrocketengine"], - packages=["openrocketengine", "openrocketengine/core"], - install_requires=["numpy", "matplotlib", "pathlib"], - entry_points={"console_scripts": []}, -) diff --git a/tests/test_core.py b/tests/test_core.py deleted file mode 100644 index a816c80..0000000 --- a/tests/test_core.py +++ /dev/null @@ -1,5 +0,0 @@ -import numpy.testing as npt - - -def test_fake_test(): - npt.assert_equal(1, 1) diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..2f6cb5c --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,412 @@ +"""Tests for the engine module.""" + +import pytest + +from openrocketengine.engine import ( + EngineGeometry, + EngineInputs, + EnginePerformance, + compute_geometry, + compute_performance, + design_engine, + format_geometry_summary, + format_performance_summary, + isp_at_altitude, + thrust_at_altitude, +) +from openrocketengine.units import ( + kelvin, + megapascals, + meters, + newtons, + pascals, +) + + +class TestEngineInputsValidation: + """Test EngineInputs validation.""" + + def test_valid_inputs(self) -> None: + """Test that valid inputs create an EngineInputs object.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + assert inputs.thrust.value == 5000 + assert inputs.gamma == 1.2 + + def test_invalid_thrust_dimension(self) -> None: + """Test that non-force thrust raises error.""" + with pytest.raises(ValueError, match="thrust must be force"): + EngineInputs( + thrust=meters(5000), # Wrong dimension! + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + def test_invalid_pressure_dimension(self) -> None: + """Test that non-pressure chamber_pressure raises error.""" + with pytest.raises(ValueError, match="chamber_pressure must be pressure"): + EngineInputs( + thrust=newtons(5000), + chamber_pressure=kelvin(2.0), # Wrong dimension! + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + def test_invalid_gamma(self) -> None: + """Test that gamma <= 1 raises error.""" + with pytest.raises(ValueError, match="gamma must be > 1"): + EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=0.9, # Invalid! + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + def test_invalid_molecular_weight(self) -> None: + """Test that negative MW raises error.""" + with pytest.raises(ValueError, match="molecular_weight must be > 0"): + EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=-10.0, # Invalid! + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + def test_invalid_mixture_ratio(self) -> None: + """Test that negative MR raises error.""" + with pytest.raises(ValueError, match="mixture_ratio must be > 0"): + EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=-1.0, # Invalid! + ) + + def test_invalid_contraction_ratio(self) -> None: + """Test that contraction ratio < 1 raises error.""" + with pytest.raises(ValueError, match="contraction_ratio must be >= 1"): + EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + contraction_ratio=0.5, # Invalid! + ) + + def test_effective_ambient_pressure_default(self) -> None: + """Test that ambient_pressure defaults to exit_pressure.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + assert inputs.effective_ambient_pressure == inputs.exit_pressure + + def test_effective_ambient_pressure_explicit(self) -> None: + """Test explicit ambient pressure.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(50000), + ambient_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + assert inputs.effective_ambient_pressure.value == 101325 + + +class TestComputePerformance: + """Test performance calculations.""" + + @pytest.fixture + def basic_inputs(self) -> EngineInputs: + """Create basic engine inputs for testing.""" + return EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + def test_performance_returns_correct_type(self, basic_inputs: EngineInputs) -> None: + """Test that compute_performance returns EnginePerformance.""" + perf = compute_performance(basic_inputs) + assert isinstance(perf, EnginePerformance) + + def test_isp_reasonable_range(self, basic_inputs: EngineInputs) -> None: + """Test that Isp is in a reasonable range for bipropellant.""" + perf = compute_performance(basic_inputs) + # Bipropellant Isp typically 250-350s at sea level + assert 200 < perf.isp.value < 400 + + def test_isp_vac_greater_than_sl(self, basic_inputs: EngineInputs) -> None: + """Test that vacuum Isp is greater than sea level Isp.""" + perf = compute_performance(basic_inputs) + assert perf.isp_vac.value > perf.isp.value + + def test_cstar_reasonable(self, basic_inputs: EngineInputs) -> None: + """Test that c* is in reasonable range.""" + perf = compute_performance(basic_inputs) + # c* typically 1500-2500 m/s + assert 1000 < perf.cstar.value < 3000 + + def test_mdot_positive(self, basic_inputs: EngineInputs) -> None: + """Test that mass flow rate is positive.""" + perf = compute_performance(basic_inputs) + assert perf.mdot.value > 0 + + def test_mdot_ox_plus_fuel_equals_total(self, basic_inputs: EngineInputs) -> None: + """Test that mdot_ox + mdot_fuel = mdot.""" + perf = compute_performance(basic_inputs) + total = perf.mdot_ox.value + perf.mdot_fuel.value + assert total == pytest.approx(perf.mdot.value) + + def test_mixture_ratio_reflected_in_flow(self, basic_inputs: EngineInputs) -> None: + """Test that O/F ratio is reflected in mass flows.""" + perf = compute_performance(basic_inputs) + computed_mr = perf.mdot_ox.value / perf.mdot_fuel.value + assert computed_mr == pytest.approx(basic_inputs.mixture_ratio, rel=1e-6) + + def test_expansion_ratio_positive(self, basic_inputs: EngineInputs) -> None: + """Test that expansion ratio is > 1.""" + perf = compute_performance(basic_inputs) + assert perf.expansion_ratio > 1 + + def test_exit_mach_supersonic(self, basic_inputs: EngineInputs) -> None: + """Test that exit Mach is supersonic.""" + perf = compute_performance(basic_inputs) + assert perf.exit_mach > 1 + + +class TestComputeGeometry: + """Test geometry calculations.""" + + @pytest.fixture + def inputs_and_performance(self) -> tuple[EngineInputs, EnginePerformance]: + """Create inputs and performance for geometry testing.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + perf = compute_performance(inputs) + return inputs, perf + + def test_geometry_returns_correct_type( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that compute_geometry returns EngineGeometry.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert isinstance(geom, EngineGeometry) + + def test_throat_area_positive( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that throat area is positive.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.throat_area.value > 0 + + def test_exit_larger_than_throat( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that exit is larger than throat.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.exit_diameter.value > geom.throat_diameter.value + + def test_chamber_larger_than_throat( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that chamber is larger than throat.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.chamber_diameter.value > geom.throat_diameter.value + + def test_contraction_ratio_matches( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that contraction ratio matches input.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.contraction_ratio == pytest.approx(inputs.contraction_ratio) + + def test_expansion_ratio_matches( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that expansion ratio matches performance.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.expansion_ratio == pytest.approx(perf.expansion_ratio) + + def test_nozzle_length_positive( + self, inputs_and_performance: tuple[EngineInputs, EnginePerformance] + ) -> None: + """Test that nozzle length is positive.""" + inputs, perf = inputs_and_performance + geom = compute_geometry(inputs, perf) + assert geom.nozzle_length.value > 0 + + +class TestDesignEngine: + """Test the convenience design_engine function.""" + + def test_design_engine_returns_both(self) -> None: + """Test that design_engine returns both perf and geometry.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + + perf, geom = design_engine(inputs) + + assert isinstance(perf, EnginePerformance) + assert isinstance(geom, EngineGeometry) + + +class TestAltitudeAnalysis: + """Test altitude performance analysis.""" + + @pytest.fixture + def full_design(self) -> tuple[EngineInputs, EnginePerformance, EngineGeometry]: + """Create complete engine design.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + perf, geom = design_engine(inputs) + return inputs, perf, geom + + def test_thrust_increases_with_altitude( + self, full_design: tuple[EngineInputs, EnginePerformance, EngineGeometry] + ) -> None: + """Test that thrust increases as ambient pressure decreases.""" + inputs, perf, geom = full_design + + thrust_sl = thrust_at_altitude(inputs, perf, geom, pascals(101325)) + thrust_high = thrust_at_altitude(inputs, perf, geom, pascals(10000)) + + assert thrust_high.value > thrust_sl.value + + def test_isp_increases_with_altitude( + self, full_design: tuple[EngineInputs, EnginePerformance, EngineGeometry] + ) -> None: + """Test that Isp increases as ambient pressure decreases.""" + inputs, perf, geom = full_design + + isp_sl = isp_at_altitude(inputs, perf, pascals(101325)) + isp_high = isp_at_altitude(inputs, perf, pascals(10000)) + + assert isp_high.value > isp_sl.value + + +class TestSummaryFormatting: + """Test summary string formatting.""" + + def test_performance_summary_contains_key_values(self) -> None: + """Test that performance summary contains expected values.""" + inputs = EngineInputs( + name="Test Engine", + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + perf = compute_performance(inputs) + + summary = format_performance_summary(inputs, perf) + + assert "Test Engine" in summary + assert "Isp" in summary + assert "Thrust" in summary + + def test_geometry_summary_contains_key_values(self) -> None: + """Test that geometry summary contains expected values.""" + inputs = EngineInputs( + name="Test Engine", + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + perf, geom = design_engine(inputs) + + summary = format_geometry_summary(inputs, geom) + summary_lower = summary.lower() + + assert "Test Engine" in summary + assert "throat" in summary_lower + assert "exit" in summary_lower + assert "chamber" in summary_lower + diff --git a/tests/test_isentropic.py b/tests/test_isentropic.py new file mode 100644 index 0000000..47faf2a --- /dev/null +++ b/tests/test_isentropic.py @@ -0,0 +1,411 @@ +"""Tests for the isentropic flow equations module.""" + +import math + +import numpy as np +import pytest + +from openrocketengine.isentropic import ( + G0_SI, + R_UNIVERSAL_SI, + area_ratio_from_mach, + bell_nozzle_length, + chamber_volume, + characteristic_velocity, + conical_nozzle_length, + diameter_from_area, + exhaust_velocity, + expansion_ratio_from_pressure_ratio, + mach_from_area_ratio_subsonic, + mach_from_area_ratio_supersonic, + mach_from_pressure_ratio, + mass_flow_rate, + mass_flow_rate_from_throat, + pressure_ratio_from_mach, + specific_gas_constant, + specific_impulse, + temperature_ratio_from_mach, + throat_area, + throat_pressure, + throat_temperature, + thrust_coefficient, + thrust_coefficient_sweep, + thrust_coefficient_vacuum, +) + + +class TestConstants: + """Test physical constants.""" + + def test_g0_si(self) -> None: + assert pytest.approx(9.80665) == G0_SI + + def test_r_universal(self) -> None: + assert pytest.approx(8314.46, rel=1e-4) == R_UNIVERSAL_SI + + +class TestGasProperties: + """Test gas property calculations.""" + + def test_specific_gas_constant_air(self) -> None: + """Test R for air (MW ≈ 28.97).""" + R = specific_gas_constant(28.97) + assert pytest.approx(287.0, rel=1e-2) == R + + def test_specific_gas_constant_h2o(self) -> None: + """Test R for water vapor (MW ≈ 18).""" + R = specific_gas_constant(18.0) + assert pytest.approx(461.9, rel=1e-2) == R + + +class TestCharacteristicVelocity: + """Test c* calculations.""" + + def test_cstar_typical_rocket(self) -> None: + """Test c* for typical rocket conditions.""" + gamma = 1.2 + R = 350.0 # J/(kg·K), typical for rocket exhaust + Tc = 3000.0 # K + + cstar = characteristic_velocity(gamma, R, Tc) + + # c* should be in the range 1500-2500 m/s for most propellants + assert 1500 < cstar < 2500 + + def test_cstar_increases_with_temperature(self) -> None: + """Test that c* increases with chamber temperature.""" + gamma = 1.2 + R = 350.0 + + cstar_low = characteristic_velocity(gamma, R, 2500.0) + cstar_high = characteristic_velocity(gamma, R, 3500.0) + + assert cstar_high > cstar_low + + def test_cstar_increases_with_R(self) -> None: + """Test that c* increases with specific gas constant (lower MW).""" + gamma = 1.2 + Tc = 3000.0 + + cstar_low = characteristic_velocity(gamma, 300.0, Tc) + cstar_high = characteristic_velocity(gamma, 400.0, Tc) + + assert cstar_high > cstar_low + + +class TestThrustCoefficient: + """Test thrust coefficient calculations.""" + + def test_cf_optimally_expanded(self) -> None: + """Test Cf when pe = pa (optimally expanded).""" + gamma = 1.2 + pe_pc = 0.01 # pe/pc + pa_pc = 0.01 # pa/pc (optimally expanded) + eps = 20.0 + + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, eps) + + # Cf should be ~1.5-1.8 for typical expansion + assert 1.4 < Cf < 2.0 + + def test_cf_overexpanded(self) -> None: + """Test Cf when pe < pa (overexpanded).""" + gamma = 1.2 + pe_pc = 0.005 + pa_pc = 0.01 # Higher ambient pressure + eps = 30.0 + + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, eps) + + # Cf should be lower due to pressure drag + assert Cf > 0 # Still positive thrust + + def test_cf_underexpanded(self) -> None: + """Test Cf when pe > pa (underexpanded).""" + gamma = 1.2 + pe_pc = 0.02 + pa_pc = 0.01 # Lower ambient pressure + eps = 10.0 + + Cf = thrust_coefficient(gamma, pe_pc, pa_pc, eps) + + # Cf should be positive + assert Cf > 0 + + def test_cf_vacuum(self) -> None: + """Test vacuum thrust coefficient.""" + gamma = 1.2 + pe_pc = 0.01 + eps = 20.0 + + Cf_vac = thrust_coefficient_vacuum(gamma, pe_pc, eps) + Cf_sl = thrust_coefficient(gamma, pe_pc, 0.01, eps) + + # Vacuum Cf should be higher than sea level + assert Cf_vac > Cf_sl + + +class TestSpecificImpulse: + """Test specific impulse calculations.""" + + def test_isp_typical_biprop(self) -> None: + """Test Isp for typical bipropellant engine.""" + cstar = 1800.0 # m/s + Cf = 1.6 + + Isp = specific_impulse(cstar, Cf, G0_SI) + + # Should be in reasonable range for biprop + assert 250 < Isp < 350 + + def test_isp_increases_with_cstar(self) -> None: + Cf = 1.6 + Isp_low = specific_impulse(1500.0, Cf, G0_SI) + Isp_high = specific_impulse(2000.0, Cf, G0_SI) + + assert Isp_high > Isp_low + + def test_isp_increases_with_cf(self) -> None: + cstar = 1800.0 + Isp_low = specific_impulse(cstar, 1.4, G0_SI) + Isp_high = specific_impulse(cstar, 1.8, G0_SI) + + assert Isp_high > Isp_low + + +class TestExhaustVelocity: + """Test exhaust velocity calculations.""" + + def test_exhaust_velocity_typical(self) -> None: + gamma = 1.2 + R = 350.0 + Tc = 3000.0 + pe_pc = 0.01 + + ue = exhaust_velocity(gamma, R, Tc, pe_pc) + + # Should be 2000-3500 m/s for typical rocket + assert 2000 < ue < 3500 + + def test_exhaust_velocity_increases_with_expansion(self) -> None: + """Test that ue increases with expansion (lower pe/pc).""" + gamma = 1.2 + R = 350.0 + Tc = 3000.0 + + ue_low_expansion = exhaust_velocity(gamma, R, Tc, 0.1) + ue_high_expansion = exhaust_velocity(gamma, R, Tc, 0.01) + + assert ue_high_expansion > ue_low_expansion + + +class TestMassFlow: + """Test mass flow rate calculations.""" + + def test_mass_flow_from_thrust_isp(self) -> None: + thrust = 50000.0 # N + Isp = 300.0 # s + + mdot = mass_flow_rate(thrust, Isp, G0_SI) + + # mdot = F / (Isp * g0) + expected = thrust / (Isp * G0_SI) + assert mdot == pytest.approx(expected) + + def test_mass_flow_from_throat(self) -> None: + """Test mass flow from throat conditions.""" + pc = 2e6 # Pa + At = 0.01 # m^2 + gamma = 1.2 + R = 350.0 + Tc = 3000.0 + + mdot = mass_flow_rate_from_throat(pc, At, gamma, R, Tc) + + assert mdot > 0 + + def test_throat_area_inverse(self) -> None: + """Test that throat_area inverts mass_flow calculation.""" + mdot = 10.0 # kg/s + cstar = 1800.0 # m/s + pc = 2e6 # Pa + + At = throat_area(mdot, cstar, pc) + + # At = mdot * cstar / pc + expected = mdot * cstar / pc + assert At == pytest.approx(expected) + + +class TestMachRelations: + """Test Mach number relations.""" + + def test_mach_from_pressure_ratio(self) -> None: + """Test Mach number from pressure ratio.""" + gamma = 1.4 + pc_p = 10.0 # stagnation/static pressure ratio + + M = mach_from_pressure_ratio(pc_p, gamma) + + assert M > 1 # Supersonic for this pressure ratio + + def test_pressure_ratio_from_mach_sonic(self) -> None: + """Test pressure ratio at sonic conditions (M=1).""" + gamma = 1.4 + + pc_p = pressure_ratio_from_mach(1.0, gamma) + + # For gamma=1.4, critical pressure ratio is ~1.893 + assert pc_p == pytest.approx(1.893, rel=1e-2) + + def test_pressure_ratio_mach_round_trip(self) -> None: + """Test that pressure ratio and Mach conversions are consistent.""" + gamma = 1.2 + M_original = 2.5 + + pc_p = pressure_ratio_from_mach(M_original, gamma) + M_recovered = mach_from_pressure_ratio(pc_p, gamma) + + assert M_recovered == pytest.approx(M_original, rel=1e-6) + + def test_temperature_ratio_sonic(self) -> None: + """Test temperature ratio at sonic conditions.""" + gamma = 1.4 + + Tc_T = temperature_ratio_from_mach(1.0, gamma) + + # For gamma=1.4, critical temperature ratio is 1.2 + assert Tc_T == pytest.approx(1.2) + + def test_area_ratio_sonic(self) -> None: + """Test area ratio at sonic conditions (should be 1).""" + gamma = 1.4 + + A_Astar = area_ratio_from_mach(1.0, gamma) + + assert A_Astar == pytest.approx(1.0) + + def test_area_ratio_supersonic(self) -> None: + """Test area ratio for supersonic flow.""" + gamma = 1.4 + + A_Astar = area_ratio_from_mach(2.0, gamma) + + assert A_Astar > 1.0 + + def test_mach_from_area_ratio_supersonic(self) -> None: + """Test supersonic Mach from area ratio.""" + gamma = 1.2 + eps = 10.0 + + M = mach_from_area_ratio_supersonic(eps, gamma) + + assert M > 1.0 + + # Verify by computing area ratio back + eps_check = area_ratio_from_mach(M, gamma) + assert eps_check == pytest.approx(eps, rel=1e-4) + + def test_mach_from_area_ratio_subsonic(self) -> None: + """Test subsonic Mach from area ratio.""" + gamma = 1.2 + eps = 2.0 + + M = mach_from_area_ratio_subsonic(eps, gamma) + + assert M < 1.0 + + # Verify by computing area ratio back + eps_check = area_ratio_from_mach(M, gamma) + assert eps_check == pytest.approx(eps, rel=1e-4) + + +class TestThroatConditions: + """Test throat (critical) conditions.""" + + def test_throat_temperature(self) -> None: + Tc = 3000.0 + gamma = 1.2 + + Tt = throat_temperature(Tc, gamma) + + assert Tt < Tc + # Tt = Tc / (1 + (gamma-1)/2) = Tc / 1.1 ≈ 2727 K + assert Tt == pytest.approx(Tc / 1.1) + + def test_throat_pressure(self) -> None: + pc = 2e6 + gamma = 1.2 + + pt = throat_pressure(pc, gamma) + + assert pt < pc + + +class TestExpansionRatio: + """Test expansion ratio calculations.""" + + def test_expansion_ratio_from_pressure_ratio(self) -> None: + pc_pe = 50.0 + gamma = 1.2 + + eps = expansion_ratio_from_pressure_ratio(pc_pe, gamma) + + assert eps > 1.0 + + +class TestGeometry: + """Test geometric calculations.""" + + def test_chamber_volume(self) -> None: + lstar = 1.0 # m + At = 0.01 # m^2 + + Vc = chamber_volume(lstar, At) + + assert Vc == pytest.approx(0.01) + + def test_diameter_from_area(self) -> None: + area = math.pi # m^2 (gives diameter of 2) + + D = diameter_from_area(area) + + assert pytest.approx(2.0) == D + + def test_conical_nozzle_length(self) -> None: + Rt = 0.05 # m + Re = 0.15 # m + half_angle = math.radians(15) + + Ln = conical_nozzle_length(Rt, Re, half_angle) + + expected = (Re - Rt) / math.tan(half_angle) + assert Ln == pytest.approx(expected) + + def test_bell_nozzle_shorter_than_conical(self) -> None: + """Test that 80% bell is shorter than conical.""" + Rt = 0.05 + Re = 0.15 + + L_conical = conical_nozzle_length(Rt, Re, math.radians(15)) + L_bell = bell_nozzle_length(Rt, Re, bell_fraction=0.8) + + assert L_bell < L_conical + assert L_bell == pytest.approx(0.8 * L_conical, rel=1e-4) + + +class TestVectorized: + """Test vectorized functions.""" + + def test_thrust_coefficient_sweep(self) -> None: + gamma = 1.2 + pe_pc = 0.01 + pa_pc_array = np.array([0.02, 0.01, 0.005, 0.001, 0.0]) + eps = 20.0 + + Cf_array = thrust_coefficient_sweep(gamma, pe_pc, pa_pc_array, eps) + + assert len(Cf_array) == len(pa_pc_array) + # Cf should increase as ambient pressure decreases + assert all(Cf_array[i] <= Cf_array[i + 1] for i in range(len(Cf_array) - 1)) + diff --git a/tests/test_nozzle.py b/tests/test_nozzle.py new file mode 100644 index 0000000..a8496d3 --- /dev/null +++ b/tests/test_nozzle.py @@ -0,0 +1,368 @@ +"""Tests for the nozzle module.""" + +import math +import tempfile +from pathlib import Path + +import numpy as np +import pytest + +from openrocketengine.engine import EngineInputs, design_engine +from openrocketengine.nozzle import ( + NozzleContour, + conical_contour, + full_chamber_contour, + generate_nozzle_from_geometry, + rao_bell_contour, +) +from openrocketengine.units import kelvin, megapascals, meters, newtons, pascals + + +class TestNozzleContour: + """Test NozzleContour dataclass.""" + + def test_create_valid_contour(self) -> None: + """Test creating a valid contour.""" + x = np.array([0.0, 0.1, 0.2]) + y = np.array([0.05, 0.06, 0.08]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + + assert len(contour.x) == 3 + assert len(contour.y) == 3 + assert contour.contour_type == "test" + + def test_mismatched_lengths_raises(self) -> None: + """Test that mismatched x and y lengths raise error.""" + x = np.array([0.0, 0.1, 0.2]) + y = np.array([0.05, 0.06]) # Different length! + + with pytest.raises(ValueError, match="same length"): + NozzleContour(x=x, y=y, contour_type="test") + + def test_too_few_points_raises(self) -> None: + """Test that contour with < 2 points raises error.""" + x = np.array([0.0]) + y = np.array([0.05]) + + with pytest.raises(ValueError, match="at least 2 points"): + NozzleContour(x=x, y=y, contour_type="test") + + def test_length_property(self) -> None: + """Test length property.""" + x = np.array([0.0, 0.1, 0.2, 0.3]) + y = np.array([0.05, 0.06, 0.07, 0.08]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + + assert contour.length == pytest.approx(0.3) + + def test_throat_radius_property(self) -> None: + """Test throat_radius property (minimum y).""" + x = np.array([0.0, 0.1, 0.2, 0.3]) + y = np.array([0.06, 0.05, 0.07, 0.08]) # Minimum at index 1 + + contour = NozzleContour(x=x, y=y, contour_type="test") + + assert contour.throat_radius == pytest.approx(0.05) + + def test_exit_radius_property(self) -> None: + """Test exit_radius property (last y value).""" + x = np.array([0.0, 0.1, 0.2, 0.3]) + y = np.array([0.05, 0.06, 0.07, 0.08]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + + assert contour.exit_radius == pytest.approx(0.08) + + def test_inlet_radius_property(self) -> None: + """Test inlet_radius property (first y value).""" + x = np.array([0.0, 0.1, 0.2, 0.3]) + y = np.array([0.05, 0.06, 0.07, 0.08]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + + assert contour.inlet_radius == pytest.approx(0.05) + + def test_to_arrays_mm(self) -> None: + """Test conversion to millimeters.""" + x = np.array([0.0, 0.001, 0.002]) # meters + y = np.array([0.001, 0.0015, 0.002]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + x_mm, y_mm = contour.to_arrays_mm() + + assert x_mm[1] == pytest.approx(1.0) # 0.001 m = 1 mm + assert y_mm[0] == pytest.approx(1.0) + + def test_to_csv(self) -> None: + """Test CSV export.""" + x = np.array([0.0, 0.01, 0.02]) + y = np.array([0.005, 0.006, 0.008]) + + contour = NozzleContour(x=x, y=y, contour_type="test") + + with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: + path = Path(f.name) + + try: + contour.to_csv(path) + + # Read and verify + content = path.read_text() + lines = content.strip().split("\n") + + assert len(lines) == 4 # Header + 3 data rows + assert "x_m" in lines[0] + assert "y_m" in lines[0] + finally: + path.unlink() + + +class TestConicalContour: + """Test conical nozzle contour generation.""" + + def test_conical_basic(self) -> None: + """Test basic conical contour generation.""" + contour = conical_contour( + throat_radius=meters(0.05), + exit_radius=meters(0.15), + half_angle=15.0, + num_points=50, + ) + + assert contour.contour_type == "conical" + assert len(contour.x) == 50 + + def test_conical_starts_at_throat(self) -> None: + """Test that conical contour starts at throat.""" + Rt = 0.05 + contour = conical_contour( + throat_radius=meters(Rt), + exit_radius=meters(0.15), + half_angle=15.0, + ) + + assert contour.x[0] == pytest.approx(0.0) + assert contour.y[0] == pytest.approx(Rt) + + def test_conical_ends_at_exit(self) -> None: + """Test that conical contour ends at exit.""" + Re = 0.15 + contour = conical_contour( + throat_radius=meters(0.05), + exit_radius=meters(Re), + half_angle=15.0, + ) + + assert contour.y[-1] == pytest.approx(Re) + + def test_conical_length_matches_geometry(self) -> None: + """Test that conical length matches geometric calculation.""" + Rt = 0.05 + Re = 0.15 + half_angle = 15.0 + + contour = conical_contour( + throat_radius=meters(Rt), + exit_radius=meters(Re), + half_angle=half_angle, + ) + + expected_length = (Re - Rt) / math.tan(math.radians(half_angle)) + assert contour.length == pytest.approx(expected_length, rel=0.01) + + def test_conical_linear_profile(self) -> None: + """Test that conical contour is linear.""" + contour = conical_contour( + throat_radius=meters(0.05), + exit_radius=meters(0.15), + half_angle=15.0, + num_points=100, + ) + + # Check linearity: y should increase linearly with x + # dy/dx should be approximately constant (tan of half angle) + expected_slope = math.tan(math.radians(15.0)) + + for i in range(1, len(contour.x)): + slope = (contour.y[i] - contour.y[i - 1]) / (contour.x[i] - contour.x[i - 1]) + assert slope == pytest.approx(expected_slope, rel=0.01) + + +class TestRaoBellContour: + """Test Rao bell nozzle contour generation.""" + + def test_rao_bell_basic(self) -> None: + """Test basic Rao bell contour generation.""" + contour = rao_bell_contour( + throat_radius=meters(0.05), + exit_radius=meters(0.15), + expansion_ratio=9.0, + bell_fraction=0.8, + num_points=100, + ) + + assert contour.contour_type == "rao_bell" + assert len(contour.x) > 0 + + def test_rao_bell_starts_near_throat(self) -> None: + """Test that Rao bell starts near throat.""" + Rt = 0.05 + contour = rao_bell_contour( + throat_radius=meters(Rt), + exit_radius=meters(0.15), + expansion_ratio=9.0, + ) + + # Should start at x=0 (throat) + assert contour.x[0] == pytest.approx(0.0, abs=1e-6) + # First y should be at throat radius + assert contour.y[0] == pytest.approx(Rt, rel=0.01) + + def test_rao_bell_ends_at_exit(self) -> None: + """Test that Rao bell ends at exit radius.""" + Re = 0.15 + contour = rao_bell_contour( + throat_radius=meters(0.05), + exit_radius=meters(Re), + expansion_ratio=9.0, + ) + + assert contour.y[-1] == pytest.approx(Re, rel=0.01) + + def test_rao_bell_shorter_than_conical(self) -> None: + """Test that 80% bell is shorter than 15° cone.""" + Rt = 0.05 + Re = 0.15 + + conical = conical_contour( + throat_radius=meters(Rt), exit_radius=meters(Re), half_angle=15.0 + ) + + bell = rao_bell_contour( + throat_radius=meters(Rt), + exit_radius=meters(Re), + expansion_ratio=9.0, + bell_fraction=0.8, + ) + + assert bell.length < conical.length + + def test_rao_bell_monotonic_y(self) -> None: + """Test that y increases monotonically (no wiggles after throat arc).""" + contour = rao_bell_contour( + throat_radius=meters(0.05), + exit_radius=meters(0.15), + expansion_ratio=9.0, + ) + + # After the initial throat arc, y should generally increase + # (allowing for small numerical variations) + mid_idx = len(contour.y) // 4 # Start checking after throat region + for i in range(mid_idx, len(contour.y) - 1): + assert contour.y[i + 1] >= contour.y[i] - 1e-10 + + +class TestGenerateNozzleFromGeometry: + """Test convenience function to generate nozzle from geometry.""" + + @pytest.fixture + def sample_geometry(self): + """Create sample engine geometry.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + _, geometry = design_engine(inputs) + return geometry + + def test_generates_valid_contour(self, sample_geometry) -> None: + """Test that function generates valid contour.""" + contour = generate_nozzle_from_geometry(sample_geometry) + + assert isinstance(contour, NozzleContour) + assert contour.contour_type == "rao_bell" + + def test_matches_geometry_dimensions(self, sample_geometry) -> None: + """Test that contour matches geometry dimensions.""" + contour = generate_nozzle_from_geometry(sample_geometry) + + Rt = sample_geometry.throat_diameter.to("m").value / 2 + Re = sample_geometry.exit_diameter.to("m").value / 2 + + # Throat radius should match + assert contour.throat_radius == pytest.approx(Rt, rel=0.05) + # Exit radius should match + assert contour.exit_radius == pytest.approx(Re, rel=0.05) + + +class TestFullChamberContour: + """Test full chamber contour generation.""" + + @pytest.fixture + def sample_design(self): + """Create sample engine design.""" + inputs = EngineInputs( + thrust=newtons(5000), + chamber_pressure=megapascals(2.0), + chamber_temp=kelvin(3200), + exit_pressure=pascals(101325), + molecular_weight=22.0, + gamma=1.2, + lstar=meters(1.0), + mixture_ratio=2.0, + ) + perf, geometry = design_engine(inputs) + return inputs, geometry + + def test_full_contour_longer(self, sample_design) -> None: + """Test that full contour is longer than nozzle alone.""" + inputs, geometry = sample_design + + nozzle_only = generate_nozzle_from_geometry(geometry) + full = full_chamber_contour(inputs, geometry, nozzle_only) + + assert full.length > nozzle_only.length + + def test_full_contour_starts_at_chamber(self, sample_design) -> None: + """Test that full contour starts at chamber radius.""" + inputs, geometry = sample_design + + nozzle_only = generate_nozzle_from_geometry(geometry) + full = full_chamber_contour(inputs, geometry, nozzle_only) + + Rc = geometry.chamber_diameter.to("m").value / 2 + + # First point should be near chamber radius + assert full.inlet_radius == pytest.approx(Rc, rel=0.1) + + def test_full_contour_includes_throat(self, sample_design) -> None: + """Test that full contour includes throat region.""" + inputs, geometry = sample_design + + nozzle_only = generate_nozzle_from_geometry(geometry) + full = full_chamber_contour(inputs, geometry, nozzle_only) + + Rt = geometry.throat_diameter.to("m").value / 2 + + # Minimum radius should be approximately throat radius + assert full.throat_radius == pytest.approx(Rt, rel=0.05) + + def test_full_contour_ends_at_exit(self, sample_design) -> None: + """Test that full contour ends at exit radius.""" + inputs, geometry = sample_design + + nozzle_only = generate_nozzle_from_geometry(geometry) + full = full_chamber_contour(inputs, geometry, nozzle_only) + + Re = geometry.exit_diameter.to("m").value / 2 + + assert full.exit_radius == pytest.approx(Re, rel=0.05) + diff --git a/tests/test_units.py b/tests/test_units.py new file mode 100644 index 0000000..236c8db --- /dev/null +++ b/tests/test_units.py @@ -0,0 +1,402 @@ +"""Tests for the units module.""" + + +import pytest + +from openrocketengine.units import ( + Quantity, + atmospheres, + bar, + centimeters, + dimensionless, + feet, + feet_per_second, + inches, + kelvin, + kg_per_second, + kilograms, + kilonewtons, + kilopascals, + megapascals, + meters, + meters_per_second, + millimeters, + newtons, + pascals, + pounds_force, + pounds_mass, + psi, + rankine, + seconds, + square_meters, +) + + +class TestQuantityCreation: + """Test Quantity creation and validation.""" + + def test_create_basic_quantity(self) -> None: + """Test creating a basic quantity.""" + q = Quantity(10.0, "m", "length") + assert q.value == 10.0 + assert q.unit == "m" + assert q.dimension == "length" + + def test_invalid_unit_raises_error(self) -> None: + """Test that invalid units raise ValueError.""" + with pytest.raises(ValueError, match="Unknown unit"): + Quantity(10.0, "invalid_unit", "length") + + def test_mismatched_dimension_raises_error(self) -> None: + """Test that mismatched dimension raises ValueError.""" + with pytest.raises(ValueError, match="has dimension"): + Quantity(10.0, "m", "pressure") # m is length, not pressure + + def test_frozen_dataclass(self) -> None: + """Test that Quantity is immutable.""" + q = meters(10.0) + with pytest.raises(AttributeError): + q.value = 20.0 # type: ignore + + +class TestFactoryFunctions: + """Test factory functions for creating quantities.""" + + def test_meters(self) -> None: + q = meters(5.0) + assert q.value == 5.0 + assert q.unit == "m" + assert q.dimension == "length" + + def test_centimeters(self) -> None: + q = centimeters(100.0) + assert q.value == 100.0 + assert q.unit == "cm" + + def test_millimeters(self) -> None: + q = millimeters(1000.0) + assert q.value == 1000.0 + assert q.unit == "mm" + + def test_feet(self) -> None: + q = feet(3.281) + assert q.value == pytest.approx(3.281) + assert q.unit == "ft" + + def test_inches(self) -> None: + q = inches(12.0) + assert q.value == 12.0 + assert q.unit == "in" + + def test_kilograms(self) -> None: + q = kilograms(100.0) + assert q.unit == "kg" + assert q.dimension == "mass" + + def test_pounds_mass(self) -> None: + q = pounds_mass(2.205) + assert q.unit == "lbm" + + def test_newtons(self) -> None: + q = newtons(1000.0) + assert q.unit == "N" + assert q.dimension == "force" + + def test_kilonewtons(self) -> None: + q = kilonewtons(1.0) + assert q.unit == "kN" + + def test_pounds_force(self) -> None: + q = pounds_force(224.8) + assert q.unit == "lbf" + + def test_pascals(self) -> None: + q = pascals(101325.0) + assert q.unit == "Pa" + assert q.dimension == "pressure" + + def test_kilopascals(self) -> None: + q = kilopascals(101.325) + assert q.unit == "kPa" + + def test_megapascals(self) -> None: + q = megapascals(0.101325) + assert q.unit == "MPa" + + def test_bar(self) -> None: + q = bar(1.01325) + assert q.unit == "bar" + + def test_psi(self) -> None: + q = psi(14.696) + assert q.unit == "psi" + + def test_atmospheres(self) -> None: + q = atmospheres(1.0) + assert q.unit == "atm" + + def test_kelvin(self) -> None: + q = kelvin(300.0) + assert q.unit == "K" + assert q.dimension == "temperature" + + def test_rankine(self) -> None: + q = rankine(540.0) + assert q.unit == "R" + + def test_meters_per_second(self) -> None: + q = meters_per_second(340.0) + assert q.unit == "m/s" + assert q.dimension == "velocity" + + def test_feet_per_second(self) -> None: + q = feet_per_second(1116.0) + assert q.unit == "ft/s" + + def test_seconds(self) -> None: + q = seconds(60.0) + assert q.unit == "s" + assert q.dimension == "time" + + def test_square_meters(self) -> None: + q = square_meters(1.0) + assert q.unit == "m^2" + assert q.dimension == "area" + + def test_kg_per_second(self) -> None: + q = kg_per_second(10.0) + assert q.unit == "kg/s" + assert q.dimension == "mass_flow" + + def test_dimensionless(self) -> None: + q = dimensionless(3.14159) + assert q.unit == "1" + assert q.dimension == "dimensionless" + + +class TestUnitConversion: + """Test unit conversion functionality.""" + + def test_meters_to_feet(self) -> None: + q = meters(1.0) + q_ft = q.to("ft") + assert q_ft.value == pytest.approx(3.28084, rel=1e-4) + assert q_ft.unit == "ft" + assert q_ft.dimension == "length" + + def test_feet_to_meters(self) -> None: + q = feet(3.28084) + q_m = q.to("m") + assert q_m.value == pytest.approx(1.0, rel=1e-4) + + def test_meters_to_centimeters(self) -> None: + q = meters(1.0) + q_cm = q.to("cm") + assert q_cm.value == pytest.approx(100.0) + + def test_pascals_to_psi(self) -> None: + q = pascals(101325.0) + q_psi = q.to("psi") + assert q_psi.value == pytest.approx(14.696, rel=1e-3) + + def test_psi_to_pascals(self) -> None: + q = psi(14.696) + q_pa = q.to("Pa") + assert q_pa.value == pytest.approx(101325.0, rel=1e-3) + + def test_newtons_to_lbf(self) -> None: + q = newtons(4.44822) + q_lbf = q.to("lbf") + assert q_lbf.value == pytest.approx(1.0, rel=1e-4) + + def test_lbf_to_newtons(self) -> None: + q = pounds_force(1.0) + q_n = q.to("N") + assert q_n.value == pytest.approx(4.44822, rel=1e-4) + + def test_to_si(self) -> None: + q = feet(10.0) + q_si = q.to_si() + assert q_si.unit == "m" + assert q_si.value == pytest.approx(3.048, rel=1e-4) + + def test_si_value_property(self) -> None: + q = feet(10.0) + assert q.si_value == pytest.approx(3.048, rel=1e-4) + + def test_conversion_incompatible_dimensions(self) -> None: + q = meters(1.0) + with pytest.raises(ValueError, match="Cannot convert between different dimensions"): + q.to("Pa") + + def test_round_trip_conversion(self) -> None: + """Test that converting back and forth preserves value.""" + original = meters(123.456) + converted = original.to("ft").to("in").to("cm").to("m") + assert converted.value == pytest.approx(original.value, rel=1e-9) + + +class TestArithmetic: + """Test arithmetic operations on quantities.""" + + def test_add_same_units(self) -> None: + q1 = meters(5.0) + q2 = meters(3.0) + result = q1 + q2 + assert result.value == 8.0 + assert result.unit == "m" + + def test_add_different_units_same_dimension(self) -> None: + q1 = meters(1.0) + q2 = centimeters(50.0) + result = q1 + q2 + assert result.value == pytest.approx(1.5) + assert result.unit == "m" + + def test_add_different_dimensions_raises(self) -> None: + q1 = meters(1.0) + q2 = pascals(100.0) + with pytest.raises(ValueError, match="different dimensions"): + q1 + q2 + + def test_subtract_same_units(self) -> None: + q1 = meters(5.0) + q2 = meters(3.0) + result = q1 - q2 + assert result.value == 2.0 + + def test_subtract_different_units_same_dimension(self) -> None: + q1 = meters(2.0) + q2 = centimeters(50.0) + result = q1 - q2 + assert result.value == pytest.approx(1.5) + + def test_multiply_by_scalar(self) -> None: + q = meters(5.0) + result = q * 2.0 + assert result.value == 10.0 + assert result.unit == "m" + + def test_rmul_scalar(self) -> None: + q = meters(5.0) + result = 2.0 * q + assert result.value == 10.0 + + def test_divide_by_scalar(self) -> None: + q = meters(10.0) + result = q / 2.0 + assert result.value == 5.0 + assert result.unit == "m" + + def test_negate(self) -> None: + q = meters(5.0) + result = -q + assert result.value == -5.0 + + def test_abs(self) -> None: + q = meters(-5.0) + result = abs(q) + assert result.value == 5.0 + + def test_multiply_length_by_length(self) -> None: + """Test that length * length = area.""" + q1 = meters(2.0) + q2 = meters(3.0) + result = q1 * q2 + assert result.dimension == "area" + assert result.value == pytest.approx(6.0) + + def test_divide_area_by_length(self) -> None: + """Test that area / length = length.""" + area = square_meters(10.0) + length = meters(2.0) + result = area / length + assert result.dimension == "length" + assert result.value == pytest.approx(5.0) + + +class TestComparison: + """Test comparison operations.""" + + def test_equality_same_units(self) -> None: + q1 = meters(5.0) + q2 = meters(5.0) + assert q1 == q2 + + def test_equality_different_units(self) -> None: + q1 = meters(1.0) + q2 = centimeters(100.0) + assert q1 == q2 + + def test_inequality(self) -> None: + q1 = meters(5.0) + q2 = meters(3.0) + assert q1 != q2 + + def test_less_than(self) -> None: + q1 = meters(3.0) + q2 = meters(5.0) + assert q1 < q2 + + def test_less_than_different_units(self) -> None: + q1 = meters(0.5) + q2 = centimeters(100.0) + assert q1 < q2 + + def test_greater_than(self) -> None: + q1 = meters(5.0) + q2 = meters(3.0) + assert q1 > q2 + + def test_less_than_or_equal(self) -> None: + q1 = meters(3.0) + q2 = meters(3.0) + assert q1 <= q2 + + def test_greater_than_or_equal(self) -> None: + q1 = meters(5.0) + q2 = meters(5.0) + assert q1 >= q2 + + def test_compare_different_dimensions_raises(self) -> None: + q1 = meters(5.0) + q2 = pascals(5.0) + with pytest.raises(ValueError, match="different dimensions"): + _ = q1 < q2 + + +class TestStringRepresentation: + """Test string representations.""" + + def test_repr(self) -> None: + q = meters(5.123456) + assert "5.12346" in repr(q) + assert "m" in repr(q) + + def test_str(self) -> None: + q = meters(5.123456) + s = str(q) + assert "5.12346" in s + assert "m" in s + + +class TestHashing: + """Test hash behavior for use in sets/dicts.""" + + def test_hash_equal_quantities(self) -> None: + """Equal quantities should have the same hash.""" + q1 = meters(5.0) + q2 = meters(5.0) + assert hash(q1) == hash(q2) + + def test_hash_equivalent_quantities(self) -> None: + """Quantities equal in SI should have the same hash.""" + q1 = meters(1.0) + q2 = centimeters(100.0) + assert hash(q1) == hash(q2) + + def test_use_in_set(self) -> None: + """Test that quantities can be used in sets.""" + s = {meters(1.0), meters(2.0), meters(1.0)} + assert len(s) == 2 + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..de6c608 --- /dev/null +++ b/uv.lock @@ -0,0 +1,916 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "beartype" +version = "0.22.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/e2/105ceb1704cb80fe4ab3872529ab7b6f365cf7c74f725e6132d0efcf1560/beartype-0.22.6.tar.gz", hash = "sha256:97fbda69c20b48c5780ac2ca60ce3c1bb9af29b3a1a0216898ffabdd523e48f4", size = 1588975, upload-time = "2025-11-20T04:47:14.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c9/ceecc71fe2c9495a1d8e08d44f5f31f5bca1350d5b2e27a4b6265424f59e/beartype-0.22.6-py3-none-any.whl", hash = "sha256:0584bc46a2ea2a871509679278cda992eadde676c01356ab0ac77421f3c9a093", size = 1324807, upload-time = "2025-11-20T04:47:11.837Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "coverage" +version = "7.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" }, + { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" }, + { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/ad/9bdc87b2eb34642c1cfe6bcb4f5db64c21f91f26b010f263e7467e7536a3/llvmlite-0.45.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:60f92868d5d3af30b4239b50e1717cb4e4e54f6ac1c361a27903b318d0f07f42", size = 43043526, upload-time = "2025-10-01T18:03:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ea/c25c6382f452a943b4082da5e8c1665ce29a62884e2ec80608533e8e82d5/llvmlite-0.45.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98baab513e19beb210f1ef39066288784839a44cd504e24fff5d17f1b3cf0860", size = 37253118, upload-time = "2025-10-01T18:04:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/fe/af/85fc237de98b181dbbe8647324331238d6c52a3554327ccdc83ced28efba/llvmlite-0.45.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3adc2355694d6a6fbcc024d59bb756677e7de506037c878022d7b877e7613a36", size = 56288209, upload-time = "2025-10-01T18:01:00.168Z" }, + { url = "https://files.pythonhosted.org/packages/0a/df/3daf95302ff49beff4230065e3178cd40e71294968e8d55baf4a9e560814/llvmlite-0.45.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f3377a6db40f563058c9515dedcc8a3e562d8693a106a28f2ddccf2c8fcf6ca", size = 55140958, upload-time = "2025-10-01T18:02:11.199Z" }, + { url = "https://files.pythonhosted.org/packages/a4/56/4c0d503fe03bac820ecdeb14590cf9a248e120f483bcd5c009f2534f23f0/llvmlite-0.45.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9c272682d91e0d57f2a76c6d9ebdfccc603a01828cdbe3d15273bdca0c3363a", size = 38132232, upload-time = "2025-10-01T18:04:52.181Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e", size = 43043524, upload-time = "2025-10-01T18:03:30.666Z" }, + { url = "https://files.pythonhosted.org/packages/9d/bc/5314005bb2c7ee9f33102c6456c18cc81745d7055155d1218f1624463774/llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a53f4b74ee9fd30cb3d27d904dadece67a7575198bd80e687ee76474620735f", size = 37253123, upload-time = "2025-10-01T18:04:18.177Z" }, + { url = "https://files.pythonhosted.org/packages/96/76/0f7154952f037cb320b83e1c952ec4a19d5d689cf7d27cb8a26887d7bbc1/llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b3796b1b1e1c14dcae34285d2f4ea488402fbd2c400ccf7137603ca3800864f", size = 56288211, upload-time = "2025-10-01T18:01:24.079Z" }, + { url = "https://files.pythonhosted.org/packages/00/b1/0b581942be2683ceb6862d558979e87387e14ad65a1e4db0e7dd671fa315/llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:779e2f2ceefef0f4368548685f0b4adde34e5f4b457e90391f570a10b348d433", size = 55140958, upload-time = "2025-10-01T18:02:30.482Z" }, + { url = "https://files.pythonhosted.org/packages/33/94/9ba4ebcf4d541a325fd8098ddc073b663af75cc8b065b6059848f7d4dce7/llvmlite-0.45.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e6c9949baf25d9aa9cd7cf0f6d011b9ca660dd17f5ba2b23bdbdb77cc86b116", size = 38132231, upload-time = "2025-10-01T18:05:03.664Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb", size = 37253121, upload-time = "2025-10-01T18:04:30.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a", size = 56288210, upload-time = "2025-10-01T18:01:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/c99c8ac7a326e9735401ead3116f7685a7ec652691aeb2615aa732b1fc4a/llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aa3dfceda4219ae39cf18806c60eeb518c1680ff834b8b311bd784160b9ce40", size = 55140957, upload-time = "2025-10-01T18:02:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" }, + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, + { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" }, +] + +[[package]] +name = "numba" +version = "0.62.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hash = "sha256:7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161", size = 2749817, upload-time = "2025-09-29T10:46:31.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/5f/8b3491dd849474f55e33c16ef55678ace1455c490555337899c35826836c/numba-0.62.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:f43e24b057714e480fe44bc6031de499e7cf8150c63eb461192caa6cc8530bc8", size = 2684279, upload-time = "2025-09-29T10:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/71969149bfeb65a629e652b752b80167fe8a6a6f6e084f1f2060801f7f31/numba-0.62.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57cbddc53b9ee02830b828a8428757f5c218831ccc96490a314ef569d8342b7b", size = 2687330, upload-time = "2025-09-29T10:43:59.601Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/403be3fecae33088027bc8a95dc80a2fda1e3beff3e0e5fc4374ada3afbe/numba-0.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:604059730c637c7885386521bb1b0ddcbc91fd56131a6dcc54163d6f1804c872", size = 3739727, upload-time = "2025-09-29T10:42:45.922Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/3d910d08b659a6d4c62ab3cd8cd93c4d8b7709f55afa0d79a87413027ff6/numba-0.62.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6c540880170bee817011757dc9049dba5a29db0c09b4d2349295991fe3ee55f", size = 3445490, upload-time = "2025-09-29T10:43:12.692Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/9d425c2f20d9f0a37f7cb955945a553a00fa06a2b025856c3550227c5543/numba-0.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:03de6d691d6b6e2b76660ba0f38f37b81ece8b2cc524a62f2a0cfae2bfb6f9da", size = 2745550, upload-time = "2025-09-29T10:44:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/30fa6873e9f821c0ae755915a3ca444e6ff8d6a7b6860b669a3d33377ac7/numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:1b743b32f8fa5fff22e19c2e906db2f0a340782caf024477b97801b918cf0494", size = 2685346, upload-time = "2025-09-29T10:43:43.677Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d5/504ce8dc46e0dba2790c77e6b878ee65b60fe3e7d6d0006483ef6fde5a97/numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90fa21b0142bcf08ad8e32a97d25d0b84b1e921bc9423f8dda07d3652860eef6", size = 2688139, upload-time = "2025-09-29T10:44:04.894Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/6a802741176c93f2ebe97ad90751894c7b0c922b52ba99a4395e79492205/numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ef84d0ac19f1bf80431347b6f4ce3c39b7ec13f48f233a48c01e2ec06ecbc59", size = 3796453, upload-time = "2025-09-29T10:42:52.771Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/efd21527d25150c4544eccc9d0b7260a5dec4b7e98b5a581990e05a133c0/numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9315cc5e441300e0ca07c828a627d92a6802bcbf27c5487f31ae73783c58da53", size = 3496451, upload-time = "2025-09-29T10:43:19.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/44/79bfdab12a02796bf4f1841630355c82b5a69933b1d50eb15c7fa37dabe8/numba-0.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:44e3aa6228039992f058f5ebfcfd372c83798e9464297bdad8cc79febcf7891e", size = 2745552, upload-time = "2025-09-29T10:44:26.399Z" }, + { url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb", size = 2685766, upload-time = "2025-09-29T10:43:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92", size = 2688741, upload-time = "2025-09-29T10:44:10.07Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7", size = 3804576, upload-time = "2025-09-29T10:42:59.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/8a1723fff71f63bbb1354bdc60a1513a068acc0f5322f58da6f022d20247/numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:728f91a874192df22d74e3fd42c12900b7ce7190b1aad3574c6c61b08313e4c5", size = 3503367, upload-time = "2025-09-29T10:43:26.326Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ec/9d414e7a80d6d1dc4af0e07c6bfe293ce0b04ea4d0ed6c45dad9bd6e72eb/numba-0.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:bbf3f88b461514287df66bc8d0307e949b09f2b6f67da92265094e8fa1282dd8", size = 2745529, upload-time = "2025-09-29T10:44:31.738Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +] + +[[package]] +name = "openrocketengine" +version = "0.2.0" +source = { editable = "." } +dependencies = [ + { name = "beartype" }, + { name = "matplotlib" }, + { name = "numba" }, + { name = "numpy" }, +] + +[package.optional-dependencies] +cea = [ + { name = "rocketcea" }, +] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.18" }, + { name = "matplotlib", specifier = ">=3.9" }, + { name = "numba", specifier = ">=0.60" }, + { name = "numpy", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, + { name = "rocketcea", marker = "extra == 'cea'", specifier = ">=1.2" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, +] +provides-extras = ["dev", "cea"] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[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 = "rocketcea" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/8b/6b48c92de8ff95f4811136a6f55ed80e5c54fae9452ebc56a9f43bb7800b/rocketcea-1.2.1.tar.gz", hash = "sha256:dd3f527089c19dfeb74d00f35bf48c2eef009a0c2f76638a72565fa9e9f9e02f", size = 70103521, upload-time = "2024-07-13T00:31:11.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/ad/0b19acf3448e2fb33cc8bd9a21f218f82454e7f796c8a0a9be7ee1dc0869/rocketcea-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:bbe88e5f51dc7a15c0655c426ffb661afe65189b1aaff46ba9b03d029200b895", size = 5350127, upload-time = "2024-07-13T00:30:42.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/2b54891da41fce035970cd16aba583b1208415e3342990b2ee7d3a803e69/rocketcea-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:e318e3df2a3a6fd1a558dbb4827230163809f4b72bcf48cc118e53b2874b2f2c", size = 5350264, upload-time = "2024-07-13T00:30:45.673Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] From dfe9c5832679e5388469a433f00d325a048a0145 Mon Sep 17 00:00:00 2001 From: Cameron Flannery Date: Tue, 25 Nov 2025 23:13:26 -0800 Subject: [PATCH 2/3] fix missing type --- openrocketengine/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openrocketengine/plotting.py b/openrocketengine/plotting.py index 8a9a7bc..81caf29 100644 --- a/openrocketengine/plotting.py +++ b/openrocketengine/plotting.py @@ -331,7 +331,7 @@ def plot_performance_vs_altitude( inputs: EngineInputs, performance: EnginePerformance, geometry: EngineGeometry, - max_altitude_km: float = 100.0, + max_altitude_km: float | int = 100.0, num_points: int = 100, figsize: tuple[float, float] = DEFAULT_FIGSIZE, ) -> Figure: @@ -428,7 +428,7 @@ def plot_performance_vs_altitude( @beartype def plot_isp_vs_expansion_ratio( - gamma: float = 1.2, + gamma: float | int = 1.2, pc_pe_range: tuple[float, float] = (10, 200), num_points: int = 100, figsize: tuple[float, float] = (10, 6), From efae267f1df832f754d17d8f002db1f6c39ee95f Mon Sep 17 00:00:00 2001 From: Cameron Flannery Date: Tue, 25 Nov 2025 23:22:31 -0800 Subject: [PATCH 3/3] update pytest --- .github/workflows/python-package.yml | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index febcc3f..8777da1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,36 +1,35 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# For more information see: https://docs.astral.sh/uv/guides/integration/github/ name: Python package on: push: - branches: [ master ] + branches: [ master, cameron/revival ] pull_request: branches: [ master ] jobs: build: - runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with black - run: | - black . --check + run: uv sync --extra dev + + - name: Lint with ruff + run: uv run ruff check . + - name: Test with pytest - run: | - pytest + run: uv run pytest