diff --git a/.gitignore b/.gitignore index e56a54c..6984350 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,11 @@ fig* *.tar *.whl +# Allow these CSVs needed for the floris interface +!MITRotor/FlorisInterface/IEA_15mw_rotor.csv + +Validation/ + ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ @@ -188,4 +193,6 @@ poetry.toml # LSP config files pyrightconfig.json -# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/python + +.DS_Store \ No newline at end of file diff --git a/MITRotor/Aerodynamics.py b/MITRotor/Aerodynamics.py index 265872c..bd761a0 100644 --- a/MITRotor/Aerodynamics.py +++ b/MITRotor/Aerodynamics.py @@ -7,7 +7,7 @@ from numpy.typing import ArrayLike from .RotorDefinition import RotorDefinition -from .Geometry import BEMGeometry +from .Geometry import BEMGeometry, expand_to_Nr_Ntheta from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw __all__ = [ @@ -269,13 +269,16 @@ def __call__( AerodynamicProperties: Calculated aerodynamic properties stored in AerodynamicProperties object. """ + tsr = expand_to_Nr_Ntheta(tsr) + pitch = expand_to_Nr_Ntheta(pitch) # calculate values in "yaw-only" frame local_yaw = -self.eff_yaw Vax = U * ((1 - an) * np.cos(local_yaw)) + theta_eff = geom.theta_mesh + self.delta_theta Vtan = ( (1 + aprime) * tsr * geom.mu_mesh - U * (1 - an) - * np.cos(self.eff_theta_mesh) + * np.cos(theta_eff) * np.sin(local_yaw) ) @@ -291,8 +294,8 @@ def __call__( an = an, aprime = aprime, solidity = solidity, - U = U * np.ones(geom.shape), - wdir = wdir * np.ones(geom.shape), + U = U * np.ones_like(geom.mu_mesh), + wdir = wdir * np.ones_like(geom.mu_mesh), Vax = Vax, Vtan = Vtan, aoa = aoa, diff --git a/MITRotor/BEMSolver.py b/MITRotor/BEMSolver.py index daea9bc..7576268 100644 --- a/MITRotor/BEMSolver.py +++ b/MITRotor/BEMSolver.py @@ -7,12 +7,13 @@ from . import Momentum, TipLoss from .Aerodynamics import AerodynamicModel, AerodynamicProperties, DefaultAerodynamics -from .Geometry import BEMGeometry +from .Geometry import BEMGeometry, expand_to_Np, expand_to_Nr_Ntheta from .RotorDefinition import RotorDefinition from .TangentialInduction import DefaultTangentialInduction, TangentialInductionModel from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw + def average(geometry: BEMGeometry, value: ArrayLike, grid: Literal["sector", "annulus", "rotor"] = "rotor"): # Assuming the function returns a 2D grid of values @@ -58,13 +59,13 @@ def solidity(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"): def U(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.U, grid) - def wdir(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"): + def wdir(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.wdir, grid) def Vax(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.Vax, grid) - def Vtan(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"): + def Vtan(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.Vtan, grid) def W(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): @@ -91,29 +92,27 @@ def Ctan(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): def Cx(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.C_x_corr, grid) - def Ctau(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"): + def Ctau(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.C_tau_corr, grid) - def Ctau_uncorr(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"): + def Ctau_uncorr(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.C_tau, grid) def F(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): return average(self.geom, self.aero_props.F, grid) def Cp(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): - dCp = ( - self.tsr - * self.geom.mu_mesh - * self.Ctau_uncorr(grid="sector") - ) + tsr = np.asarray(self.tsr) + if tsr.ndim == 1: + tsr = expand_to_Nr_Ntheta(tsr) + dCp = (tsr * self.geom.mu_mesh * self.Ctau_uncorr(grid="sector")) return average(self.geom, dCp, grid=grid) def Cp_corr(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): - dCp = ( - self.tsr - * self.geom.mu_mesh - * self.Ctau(grid="sector") - ) + tsr = np.asarray(self.tsr) + if tsr.ndim == 1: + tsr = expand_to_Nr_Ntheta(tsr) + dCp = (tsr * self.geom.mu_mesh * self.Ctau(grid="sector")) return average(self.geom, dCp, grid=grid) def Ct(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"): @@ -169,21 +168,37 @@ def sample_points(self, yaw: float = 0.0, tilt: float = 0.0) -> tuple[ArrayLike, return X, Y, Z def pre_process(self, pitch, tsr, yaw = 0, tilt = 0, **kwargs): + pitch, tsr = np.asarray(pitch), np.asarray(tsr) + yaw, tilt = np.asarray(yaw), np.asarray(tilt) + self.scalar_inputs = (pitch.ndim == 0) & (tsr.ndim == 0) & (yaw.ndim == 0) & (tilt.ndim == 0) + if not self.scalar_inputs: + assert len(pitch) == len(tsr) == len(yaw) == len(tilt), "Setpoint arrays should be the same lenght" # switch reference frame to a "yaw-only" frame where y' is aligned with the lateral wake - self.aerodynamic_model.eff_yaw = calc_eff_yaw(yaw, tilt) - if tilt == 0: - dtheta = 0 - elif yaw == 0: - dtheta = np.pi / 2 - else: # non-zero yaw and tilt - sin_eff = np.sin(self.aerodynamic_model.eff_yaw) - dtheta = np.arccos(np.sin(yaw) / sin_eff) - self.aerodynamic_model.eff_theta_mesh = self.geometry.theta_mesh + dtheta + yaw, tilt = np.broadcast_arrays(yaw, tilt) + eff_yaw = calc_eff_yaw(yaw, tilt) + # initialize dtheta + dtheta = np.zeros_like(eff_yaw, dtype=float) + # masks + yaw_zero = (yaw == 0) + not_tilt_zero = np.logical_not(tilt == 0) + # case1: yaw == 0 and tilt != 0 + case1 = yaw_zero & not_tilt_zero + dtheta[case1] = np.pi / 2 + # case2: yaw != 0 and tilt != 0 + case2 = np.logical_not(yaw_zero) & not_tilt_zero + dtheta[case2] = np.arccos( + np.sin(yaw[case2]) / np.sin(eff_yaw[case2]) + ) + # expand everything to the correct dimensions to ensure broadcasting + self.aerodynamic_model.eff_yaw = expand_to_Nr_Ntheta(eff_yaw) + self.aerodynamic_model.delta_theta = expand_to_Nr_Ntheta(dtheta) + self.geometry.mu_mesh = expand_to_Np(self.geometry.mu_mesh) + self.geometry.theta_mesh = expand_to_Np(self.geometry.theta_mesh) return def initial_guess(self, *args, **kwargs) -> Tuple[ArrayLike, ...]: - a = (1 / 3) * np.ones(self.geometry.shape) - aprime = np.zeros(self.geometry.shape) + a = (1 / 3) * np.ones_like(self.geometry.mu_mesh) + aprime = np.zeros_like(self.geometry.mu_mesh) return a, aprime @@ -198,8 +213,8 @@ def residual( tilt: ArrayLike = 0.0, ) -> Tuple[ArrayLike, ...]: an, aprime = x - U = np.ones(self.geometry.shape) if U is None else U - wdir = np.zeros(self.geometry.shape) if wdir is None else wdir + U = np.ones_like(self.geometry.mu_mesh) if U is None else U + wdir = np.zeros_like(self.geometry.mu_mesh) if wdir is None else wdir aero_props = self.aerodynamic_model( an = an, @@ -221,12 +236,25 @@ def residual( return e_an, e_aprime def post_process(self, result: FixedPointIterationResult, pitch, tsr, yaw = 0, U=None, wdir=None, tilt = 0.0) -> BEMSolution: - U = np.ones(self.geometry.shape) if U is None else U - wdir = np.zeros(self.geometry.shape) if wdir is None else wdir + U = np.ones_like(self.geometry.mu_mesh) if U is None else U + wdir = np.zeros_like(self.geometry.mu_mesh) if wdir is None else wdir an, aprime = result.x aero_props = self.aerodynamic_model(an, aprime, pitch, tsr, yaw, self.rotor, self.geometry, U, wdir, tilt = tilt) aero_props.F = self.tiploss_model(aero_props, pitch, tsr, yaw, self.rotor, self.geometry, tilt = tilt) avg_Ct = average(self.geometry, aero_props.C_x) u4,v4,w4 = self.momentum_model.compute_initial_wake_velocities(avg_Ct, yaw, tilt = tilt) + if self.scalar_inputs: # if all setpoints were scalars + # return single values as scalars + pitch, tsr, yaw, tilt = [np.asarray(x).item() for x in (pitch, tsr, yaw, tilt)] + u4, v4, w4 = [np.asarray(x).item() for x in (u4, v4, w4)] + # remove unneeded extra axis from Ntheta x Nr arrays + an = np.squeeze(an) + aprime = np.squeeze(aprime) + self.geometry.theta_mesh = np.squeeze(self.geometry.theta_mesh) + self.geometry.mu_mesh = np.squeeze(self.geometry.mu_mesh) + for key, value in vars(aero_props).items(): + if isinstance(value, np.ndarray): + setattr(aero_props, key, np.squeeze(value)) + return BEMSolution(pitch, tsr, yaw, aero_props, self.geometry, result.converged, result.niter, u4, v4, tilt = tilt, w4 = w4) diff --git a/MITRotor/FlorisInterface/FlorisInterface.py b/MITRotor/FlorisInterface/FlorisInterface.py new file mode 100644 index 0000000..50c2646 --- /dev/null +++ b/MITRotor/FlorisInterface/FlorisInterface.py @@ -0,0 +1,136 @@ +import os +import numpy as np +import polars as pl +from attrs import define, field +from typing import Optional +from scipy.interpolate import interp1d +# FLORIS Imports +from floris.type_dec import floris_float_type, NDArrayFloat +from floris.core.turbine.operation_models import BaseOperationModel +from floris.core.rotor_velocity import average_velocity +# MITRotor / UMM Imports +from MITRotor.ReferenceTurbines import IEA15MW +from MITRotor.Momentum import UnifiedMomentum +from MITRotor.Geometry import BEMGeometry +from MITRotor.TipLoss import NoTipLoss +from MITRotor.BEMSolver import BEM + +# default rotor if none provided by user (IEA 15MW) +def default_bem_factory(): + return BEM( + rotor=IEA15MW(), + momentum_model=UnifiedMomentum(averaging="rotor"), + geometry=BEMGeometry(Nr=10, Ntheta=20), + tiploss_model=NoTipLoss() + ) +# pitch vs windspeed interpolater if none provided by user +# for IEA 15MW from figure 2 (https://docs.nrel.gov/docs/fy22osti/82134.pdf) +def default_pitch_interp(): + module_dir = os.path.dirname(__file__) + pitch_file = os.path.join(module_dir, "IEA_15mw_rotor.csv") + df = pl.read_csv(pitch_file) + wind_table = df["Wind [m/s]"].to_numpy() + pitch_table = df["Pitch [deg]"].to_numpy() + # TODO: should fill_value be extrapolate? + return interp1d(wind_table, pitch_table, kind="linear", fill_value="extrapolate", bounds_error=False) + +# tsr vs windspeed interpolater if none provided by user +# for IEA 15MW from figure 2 (https://docs.nrel.gov/docs/fy22osti/82134.pdf) +def default_tsr_interp(): + module_dir = os.path.dirname(__file__) + tsr_file = os.path.join(module_dir, "IEA_15mw_rotor.csv") + df = pl.read_csv(tsr_file) + wind_table = df["Wind [m/s]"].to_numpy() + tip_speed_table = df["Tip Speed [m/s]"].to_numpy() + tsr_table = tip_speed_table / wind_table + # TODO: should fill_value be extrapolate? + return interp1d(wind_table, tsr_table, kind="linear", fill_value="extrapolate", bounds_error=False) + +@define +class MITRotorTurbine(BaseOperationModel): + """ + Turbine operation model as described by Liew et al. (2024). + + Args: + bem_model (BEM): optional BEM model as defined in MITRotor, defaults to IEA15MW with UMM momentum model + pitch_csv (str): optional path to pitch trajectory based on wind speed, defaults to IEA15MW Figure 2 (https://docs.nrel.gov/docs/fy22osti/82134.pdf) + tsr_csv (str)): optional path to tsr trajectory based on wind speed, defaults to IEA15MW Figure 2 (https://docs.nrel.gov/docs/fy22osti/82134.pdf) + + Methods: + power + thrust_coefficient + axial_induction + """ + # user can define a BEM model if they want a different rotor, momentum model, or geometry + bem_model = field(init = True, factory = default_bem_factory, type = BEM) + + # create interp objects based on pitch and tsr csvs + pitch_interp = field(init=True, factory=default_pitch_interp, type = interp1d, repr = False) + tsr_interp = field(init=True, factory=default_tsr_interp, type = interp1d, repr = False) + + # save most recent solution by unique floris arguments + _last_key = field(init=False, default=None, type = bytes) + _a = field(init=False, default=None, type = NDArrayFloat) + _Ct = field(init=False, default=None, type = NDArrayFloat) + _power = field(init=False, default=None, type = NDArrayFloat) + + def _get_state_key(self, velocities: np.ndarray, yaw_angles: np.ndarray, tilt_angles: np.ndarray) -> tuple: + # saves key to uniquely identify farm state -> avoids re-solving for calls to power, thrust, and induction for same state + return velocities.tobytes(), yaw_angles.tobytes(), tilt_angles.tobytes() + + def _update_solution(self, + velocities: NDArrayFloat, + air_density: float, + yaw_angles: NDArrayFloat, + tilt_angles: NDArrayFloat, + average_method: str = "cubic-mean", + cubature_weights: Optional[NDArrayFloat] = None, + **_, + ): + # create cache key for current inputs + key = self._get_state_key(velocities, yaw_angles, tilt_angles) + # update solution if conditions are different + if key != self._last_key: + n_findex, n_turbines = yaw_angles.shape + + # save new key and clear fields + self._last_key = key + self._a = np.empty((n_findex, n_turbines), dtype=floris_float_type) + self._Ct = np.empty((n_findex, n_turbines), dtype=floris_float_type) + self._power = np.empty((n_findex, n_turbines), dtype=floris_float_type) + + # compute the power-effective wind speed across the rotor + rotor_average_velocities = average_velocity( + velocities=velocities, + method=average_method, + cubature_weights=cubature_weights, + ) + + # calculate rotor area + rotor_area = np.pi * self.bem_model.rotor.R**2 + + # get setpoints + yaw, tilt = np.deg2rad(yaw_angles), np.deg2rad(tilt_angles) + pitch = np.deg2rad(self.pitch_interp(rotor_average_velocities)) + tsr = self.tsr_interp(rotor_average_velocities) + for tindex in range(n_turbines): + # solve BEM + bem_sol = self.bem_model(pitch[:, tindex], tsr[:, tindex], yaw = yaw[:, tindex], tilt = tilt[:, tindex]) + # get induction and thrust coeff + self._a[:, tindex] = bem_sol.a() + self._Ct[:, tindex] = bem_sol.Ct() + # compute power + self._power[:, tindex] = 0.5 * bem_sol.Cp() * air_density * rotor_area * (rotor_average_velocities[:, tindex])**3 + return + + def power(self, **kwargs) -> NDArrayFloat: + self._update_solution(**kwargs) + return self._power + + def thrust_coefficient(self, **kwargs) -> NDArrayFloat: + self._update_solution(**kwargs) + return self._Ct + + def axial_induction(self, **kwargs) -> NDArrayFloat: + self._update_solution(**kwargs) + return self._a \ No newline at end of file diff --git a/MITRotor/FlorisInterface/IEA_15mw_rotor.csv b/MITRotor/FlorisInterface/IEA_15mw_rotor.csv new file mode 100644 index 0000000..c5ce68c --- /dev/null +++ b/MITRotor/FlorisInterface/IEA_15mw_rotor.csv @@ -0,0 +1,51 @@ +Wind [m/s],Pitch [deg],Power [MW],Power Coefficient [-],Aero Power Coefficient [-],Rotor Speed [rpm],Tip Speed [m/s],Thrust [MN],Thrust Coefficient [-],Torque [MNm],Torque Coefficient [-],Blade Moment [MNm],Blade Moment Coefficient [-] +3,3.920293066,0.042500121,0.056434434,0.05897929,5,63.33974388,0.202909425,0.808309128,0.0848295,0.002806784,5.980185544,0.197868559 +3.54953237,3.913016973,0.292273273,0.234311418,0.244877465,5,63.33974388,0.275784048,0.784774049,0.583372359,0.01378822,7.854972515,0.185655162 +4.067900771,3.709535473,0.607683429,0.323656337,0.338251306,5,63.33974388,0.360568653,0.781205046,1.212925534,0.021827203,10.04565323,0.180776562 +4.553906848,3.347852063,0.980824467,0.372354525,0.389145491,5,63.33974388,0.454006762,0.784895445,1.957708542,0.028111524,12.46569389,0.178999911 +5.006427063,2.905271743,1.401604341,0.400460354,0.418518725,5,63.33974388,0.550743112,0.787791082,2.797577836,0.033237705,14.97412576,0.177905892 +5.424415288,2.410742282,1.858097567,0.41737602,0.436197187,5,63.33974388,0.648091847,0.789675144,3.708730359,0.03753393,17.50236286,0.177131362 +5.806905228,1.897891096,2.33681832,0.42786672,0.447160954,5,63.33974388,0.743097627,0.790085645,4.66424864,0.041190481,19.97338486,0.176387107 +6.153012649,1.38709008,2.823250666,0.434514015,0.454108003,5,63.33974388,0.833653636,0.789455775,5.635159124,0.044323619,22.33283369,0.175659992 +6.461937428,0.891713024,3.302257138,0.438773526,0.458559593,5,63.33974388,0.918175231,0.78834778,6.591247694,0.047005295,24.54016065,0.175007454 +6.732965398,0.425207394,3.758708724,0.441506699,0.461416015,5,63.33974388,0.995161603,0.787043287,7.502317104,0.049281883,26.55670159,0.174448007 +6.965470002,0.000469162,4.17814357,0.443251132,0.463239112,5,63.33974388,1.063280746,0.785714686,8.339501745,0.05118514,28.34690587,0.173984056 +7.158913742,0,4.546959581,0.444321653,0.46363055,5.086081797,64.43022368,1.11334287,0.778847528,8.908071675,0.051759978,29.65857718,0.172329924 +7.312849418,0,4.855095716,0.445098035,0.46363055,5.195446076,65.81564476,1.161737302,0.778847528,9.295284892,0.051759978,30.94776674,0.172329924 +7.426921164,0,5.091278414,0.445572294,0.46363055,5.27648885,66.84229047,1.198263417,0.778847528,9.587537406,0.051759978,31.92079369,0.172329924 +7.500865272,0,5.248186603,0.445854287,0.46363055,5.329022767,67.50778745,1.22224256,0.778847528,9.779399169,0.051759978,32.55957918,0.172329924 +7.534510799,0,5.320523056,0.445971313,0.46363055,5.35292638,67.81059719,1.233232019,0.778847528,9.867327955,0.051759978,32.8523297,0.172329924 +7.541241633,0,5.335074621,0.445994704,0.46363055,5.357708331,67.8711747,1.23543638,0.778847528,9.884965469,0.051759978,32.91105213,0.172329924 +7.58833327,0,5.437629628,0.44615751,0.46363055,5.391164791,68.29499943,1.250914035,0.778847528,10.00880519,0.051759978,33.32336468,0.172329924 +7.675676842,0,5.630967375,0.446427293,0.46363055,5.453218417,69.08109158,1.279876417,0.778847528,10.2405388,0.051759978,34.09489971,0.172329924 +7.803070431,0,5.920680499,0.446779068,0.46363055,5.543725753,70.22763388,1.322713319,0.778847528,10.58328514,0.051759978,35.23604104,0.172329924 +7.970219531,0,6.314795749,0.44716338,0.46363055,5.662477568,71.73197578,1.379987779,0.778847528,11.04154918,0.051759978,36.76178755,0.172329924 +8.176737731,0,6.824124693,0.447532442,0.46363055,5.809199332,73.59063957,1.452428655,0.778847528,11.62116264,0.051759978,38.69155545,0.172329924 +8.422147605,0,7.462468983,0.447849163,0.46363055,5.983551858,75.79932844,1.540920985,0.778847528,12.3292069,0.051759978,41.04892142,0.172329924 +8.70588182,0,8.237943086,0.447608431,0.46363055,6.185132081,78.35293638,1.646494209,0.778847528,13.17391868,0.051759978,43.86130896,0.172329924 +9.027284445,0,9.167503057,0.446783839,0.46363055,6.413473991,81.24556,1.770308387,0.778847528,14.16457987,0.051759978,47.15962114,0.172329924 +9.385612468,0,10.28468981,0.445985887,0.46363055,6.668049714,84.47051221,1.913638541,0.778847528,15.31139216,0.051759978,50.97782356,0.172329924 +9.780037514,0,11.61664761,0.44522242,0.46363055,6.948270725,88.02033763,2.077857279,0.778847528,16.62533805,0.051759978,55.3524814,0.172329924 +10.20964776,0,13.19374497,0.444481389,0.46363055,7.253489215,91.88682983,2.264415873,0.778847528,18.11802945,0.051759978,60.3222554,0.172329924 +10.65843263,0,15,0.444149321,0.463833394,7.499240933,95,2.447339849,0.772369945,19.94703495,0.05228731,65.14869599,0.170774759 +10.67345004,0.511974392,15.00000265,0.442277294,0.461878402,7.499240933,95,2.374311052,0.74721528,19.9470383,0.052140287,63.06741054,0.164854194 +11.17037214,3.723733263,14.99997215,0.385838268,0.402938078,7.499240933,95,1.956901748,0.562278542,19.94699774,0.047604378,50.90543244,0.12148803 +11.6992653,5.396486368,15.00000888,0.33584085,0.350724844,7.499240933,95,1.769139016,0.463406962,19.94704659,0.043397635,45.21424327,0.098370013 +12.25890683,6.766694447,15.00000059,0.291913256,0.304850441,7.499240933,95,1.630599159,0.389010657,19.94703557,0.039525695,40.89463241,0.081034035 +12.84800295,7.989590009,15.0000001,0.2535725,0.264810476,7.499240933,95,1.518234459,0.329750424,19.94703492,0.03598419,37.29572857,0.067281007 +13.46519181,9.124292675,14.99994632,0.220277351,0.230039733,7.499240933,95,1.423067352,0.281396143,19.9469634,0.032760939,34.16497221,0.05611263 +14.10904661,10.20005088,15.00008104,0.191477776,0.199963801,7.499240933,95,1.340503102,0.241429458,19.94714255,0.029839394,31.37410139,0.046933247 +14.77807889,11.23370987,15.00005146,0.166631299,0.174016164,7.499240933,95,1.267744754,0.208119912,19.94710322,0.027198723,28.84591141,0.039332626 +15.470742,12.23548853,15.00003481,0.145236779,0.151673469,7.499240933,95,1.20299036,0.180201181,19.94708107,0.02481771,26.53102258,0.033009302 +16.18543466,13.21207233,15.00002476,0.12683428,0.132455397,7.499240933,95,1.144953313,0.156695606,19.94706771,0.022674357,24.39444366,0.027729806 +16.92050464,14.16785471,15.00001806,0.111011922,0.115931814,7.499240933,95,1.092675034,0.136830266,19.9470588,0.020747076,22.41059375,0.023309415 +17.67425264,15.10569973,15.00001315,0.097406118,0.101723021,7.499240933,95,1.045410488,0.119983771,19.94705227,0.019015215,20.56016302,0.019599684 +18.44493615,16.02744622,15.00000923,0.08569941,0.089497487,7.499240933,95,1.002561347,0.105651196,19.94704705,0.017459385,18.82803369,0.016479927 +19.23077353,16.93422766,15.000006,0.075616913,0.078968149,7.499240933,95,0.963634907,0.093419339,19.94704276,0.016061631,17.20197892,0.013851268 +20.02994808,17.82668205,15.00000341,0.066922115,0.069888009,7.499240933,95,0.928217511,0.082948375,19.94703931,0.014805512,15.6718209,0.011632269 +20.8406123,18.70508213,15.00000149,0.059412476,0.062045553,7.499240933,95,0.895956018,0.07395772,19.94703676,0.013676094,14.22892793,0.009755642 +21.66089211,19.56943768,15.00000033,0.052915149,0.055260274,7.499240933,95,0.866544894,0.066214966,19.94703522,0.012659901,12.8658372,0.008165636 +22.4888912,20.41950905,15.00018216,0.047283561,0.049379102,7.499240933,95,0.839725354,0.059527679,19.94727702,0.011744977,11.57634071,0.006816161 +23.32269542,21.25504455,15.00000054,0.042390918,0.044269624,7.499240933,95,0.815233262,0.053733135,19.9470355,0.010920072,10.35411323,0.005668395 +24.1603772,22.07544699,15.00000177,0.038132735,0.039822724,7.499240933,95,0.79288419,0.048699008,19.94703713,0.010175965,9.195130033,0.004690888 +25,22.88018135,15.0000035,0.034418276,0.035943645,7.499240933,95,0.772480289,0.044312389,19.94703943,0.009503927,8.095078123,0.003856965 \ No newline at end of file diff --git a/MITRotor/Geometry.py b/MITRotor/Geometry.py index 8a00144..7f0ae3f 100644 --- a/MITRotor/Geometry.py +++ b/MITRotor/Geometry.py @@ -40,14 +40,53 @@ def cartesian(self, yaw: float, tilt: float) -> Tuple[ArrayLike, ...]: Z = self.mu_mesh * np.cos(self.theta_mesh) # vertical return X, Y, Z + +# ---------- Function for averaging over the rotor ---------- # - def annulus_average(self, X: ArrayLike): - X_azim = 1 / (2 * np.pi) * np.trapezoid(X, self.theta_mesh, axis=-1) + def annulus_average(self, X): + X = np.asarray(X) + theta = np.asarray(self.theta).reshape(-1) + #Ensure last axis is Nθ + if X.shape[-1] != theta.shape[0]: + raise ValueError(f"Mismatch: X.shape={X.shape}, theta.shape={theta.shape}") + # Integrate over Nθ (last axis) + return (1 / (2 * np.pi)) * np.trapezoid(X, theta, axis=-1) - return X_azim + def rotor_average(self, X): + X = np.asarray(X) + mu = np.asarray(self.mu).reshape(-1) # (Nr,) + # Ensure last axis is Nr + if X.shape[-1] != mu.shape[0]: + raise ValueError(f"Mismatch: X.shape={X.shape}, mu.shape={mu.shape}") + # Integrate over Nr (last axis) + return 2 * np.trapezoid(X * mu, mu, axis=-1) + +# ---------- Function for adjusting axes for vectorization ---------- # - def rotor_average(self, X: ArrayLike): - # Takes annulus average quantities and performs rotor average +def expand_to_Np(x): # add setpoint axis + x = np.atleast_1d(x) + if x.ndim < 3: + return x[None, ...] + else: + return x - X_rotor = 2 * np.trapezoid(X * self.mu, self.mu) - return X_rotor +def expand_to_Nr_Ntheta(x): # add Nr and Nθ axis + x = np.atleast_1d(x) + if x.ndim < 3: + return x[:, None, None] + else: + return x + +def expand_to_Nr(x): # add Nr axis + x = np.atleast_1d(x) + if x.ndim < 2: + return x[:, None] + else: + return x + +def expand_to_Ntheta(x): # add Nθ axis + x = np.atleast_1d(x) + if x.ndim < 3: + return x[:, :, None] + else: + return x diff --git a/MITRotor/Momentum.py b/MITRotor/Momentum.py index 6fc4646..6834ff0 100644 --- a/MITRotor/Momentum.py +++ b/MITRotor/Momentum.py @@ -8,6 +8,7 @@ from pathlib import Path from UnifiedMomentumModel import Momentum as UMM from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw, eff_yaw_inv_rotation +from .Geometry import expand_to_Nr_Ntheta, expand_to_Nr, expand_to_Ntheta if TYPE_CHECKING: from .Geometry import BEMGeometry @@ -62,13 +63,11 @@ def _func_rotor( geom.rotor_average( geom.annulus_average( np.clip(aero_props.C_x_corr, 0, 1.69) - ) - ) + ) + ) ) - - return self.compute_induction(rotor_avg_axial_force, yaw = yaw, tilt = tilt) - - + an = self.compute_induction(rotor_avg_axial_force, yaw = yaw, tilt = tilt) + return expand_to_Nr_Ntheta(an) def _func_annulus( self, @@ -81,15 +80,12 @@ def _func_annulus( tilt: float = 0.0, ) -> ArrayLike: - annulus_avg_axial_force = ( - - geom.annulus_average( - np.clip(aero_props.C_x_corr, -10, 10) - ) - )[:, None] * np.ones(geom.shape) - - - return self.compute_induction(annulus_avg_axial_force, yaw = yaw, tilt = tilt) + annulus_avg_axial_force = geom.annulus_average( + np.clip(aero_props.C_x_corr, 0, 1.69) + ) + yaw, tilt = expand_to_Nr(yaw), expand_to_Nr(tilt) + an = self.compute_induction(annulus_avg_axial_force, yaw = yaw, tilt = tilt) + return expand_to_Ntheta(an) def _func_sector( self, @@ -102,7 +98,7 @@ def _func_sector( tilt: float = 0.0, ) -> ArrayLike: axial_force = np.clip(aero_props.C_x_corr, -10, 10) - + yaw, tilt = expand_to_Nr_Ntheta(yaw), expand_to_Nr_Ntheta(tilt) return self.compute_induction(axial_force, yaw = yaw, tilt = tilt) def __call__( @@ -188,8 +184,8 @@ def __init__(self, def compute_induction(self, Cx: ArrayLike, yaw: float, tilt: float = 0.0) -> ArrayLike: - if tilt != 0: - raise ValueError("Tilt not supported by the Madsen momentum model. Use UMM.") + # if tilt != 0: + # raise ValueError("Tilt not supported by the Madsen momentum model. Use UMM.") if self.cosine_exponent: Ct = Cx / (np.cos(yaw)**2) else: diff --git a/MITRotor/TangentialInduction.py b/MITRotor/TangentialInduction.py index cd493ec..d2dff09 100644 --- a/MITRotor/TangentialInduction.py +++ b/MITRotor/TangentialInduction.py @@ -3,7 +3,7 @@ from numpy.typing import ArrayLike from .Aerodynamics import AerodynamicProperties -from .Geometry import BEMGeometry +from .Geometry import BEMGeometry, expand_to_Nr_Ntheta from .RotorDefinition import RotorDefinition from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw @@ -54,7 +54,8 @@ def __call__( geom: BEMGeometry, tilt: float = 0.0, ) -> ArrayLike: - eff_yaw = calc_eff_yaw(yaw, tilt) + eff_yaw = expand_to_Nr_Ntheta(calc_eff_yaw(yaw, tilt)) + tsr = expand_to_Nr_Ntheta(tsr) aprime = ( np.clip(aero_props.C_tau_corr, -2, 2) / (4 * np.maximum(geom.mu_mesh, 0.1) ** 2 * tsr * (1 - aero_props.an) * np.cos(eff_yaw)) diff --git a/examples/example_02_rotor_distributions.py b/examples/example_02_rotor_distributions.py index 517ea21..98e976f 100644 --- a/examples/example_02_rotor_distributions.py +++ b/examples/example_02_rotor_distributions.py @@ -17,7 +17,7 @@ def plot_radial_distributions(sol: BEMSolution, save_to: Path): [ax.legend(loc="lower center") for ax in axes] - axes[-1].set_xlabel("Radial position, $\mu$ [-]") + axes[-1].set_xlabel("Radial position, $\\mu$ [-]") plt.xlim(0, 1) plt.savefig(save_to, dpi=300, bbox_inches="tight") diff --git a/examples/example_04_yaw_tilt_rotor_comparison.py b/examples/example_04_yaw_tilt_rotor_comparison.py index 5b39520..9a809bf 100644 --- a/examples/example_04_yaw_tilt_rotor_comparison.py +++ b/examples/example_04_yaw_tilt_rotor_comparison.py @@ -41,7 +41,7 @@ axes[i, j].set_title(f"r/R ={np.round(r_mesh[0], decimals=2)}") fig.legend( - [f"Yaw ${rounded_deg_misalignment}^\circ$", f"Tilt ${rounded_deg_misalignment}^\circ$", f"Yaw ${np.rad2deg(yaw)}^\circ$ and Tilt ${np.rad2deg(yaw)}^\circ$"], # labels + [f"Yaw ${rounded_deg_misalignment}^\\circ$", f"Tilt ${rounded_deg_misalignment}^\\circ$", f"Yaw ${np.rad2deg(yaw)}^\\circ$ and Tilt ${np.rad2deg(yaw)}^\\circ$"], # labels loc='lower center', ncol=3, bbox_to_anchor=(0.5, 0.05) diff --git a/examples/example_06_floris_integration.py b/examples/example_06_floris_integration.py new file mode 100644 index 0000000..e65bf3a --- /dev/null +++ b/examples/example_06_floris_integration.py @@ -0,0 +1,377 @@ +import os +import numpy as np +import polars as pl +from pathlib import Path +import time +import warnings + +warnings.filterwarnings("ignore", category=RuntimeWarning) # JUST FOR NOW + + +import matplotlib.pyplot as plt +from matplotlib import cm +from matplotlib.colors import Normalize +from matplotlib.cm import ScalarMappable + +# Floris imports +from floris import FlorisModel, TimeSeries + +# MITRotor / UMM Imports +from MITRotor.FlorisInterface.FlorisInterface import MITRotorTurbine, default_bem_factory, default_pitch_interp, default_tsr_interp +from MITRotor.ReferenceTurbines import IEA15MW +from MITRotor.Momentum import UnifiedMomentum, UnifiedMomentumLUT +from MITRotor.Geometry import BEMGeometry +from MITRotor.TipLoss import NoTipLoss +from MITRotor.BEMSolver import BEM + +figdir = Path("fig") +floris_air_density = 1.225 +# ------------------ run basic case -------------------------------------------------------- +fmodel = FlorisModel("defaults") +time_series = TimeSeries( + wind_directions=np.array([270.0, 270.0, 280.0]), + wind_speeds=np.array([8.0, 10.0, 12.0]), + turbulence_intensities=np.array([0.06, 0.06, 0.06]), +) +yaw_angles = np.array([ + [0.0, 0.0], # condition 1 + [0.0, 0.0], # condition 2 + [0.0, 0.0], # condition 3 +]) + +fmodel.set( + layout_x = [0.0, 500.0], + layout_y = [0.0, 0.0], + wind_data = time_series, + yaw_angles = yaw_angles +) +fmodel.set_operation_model(MITRotorTurbine()) +fmodel.run() +print("Powers [W]:\n", fmodel.get_turbine_powers(), "\n") +print("Thrust coefficients [-]:\n", fmodel.get_turbine_thrust_coefficients(), "\n") +print("Axial induction factors [-]:\n", fmodel.get_turbine_axial_induction_factors(), "\n") + +# -------------------- plot pitch and tsr control curves, as well as CT for IEA15MW ------------------ +# Credit to Ilan Upfal for initial validation of MITRotor vs IEA15MW and much of the script below. +# Floris Interface written and tested by Skylar Gering. +module_dir = os.path.dirname(__file__) # examples/ +csv_file = os.path.join(module_dir, "..", "MITRotor", "FlorisInterface", "IEA_15mw_rotor.csv") +df = pl.read_csv(csv_file) + +wind_table = df["Wind [m/s]"].to_numpy() +wind_speeds = np.linspace(5, 25, 20) +wind_dirs = np.full_like(wind_speeds, 270.0) +turbulence_intensity = np.zeros_like(wind_speeds) + +# ------- plot pitch and tsr control curves for IEA15MW from figure 2 (https://docs.nrel.gov/docs/fy22osti/82134.pdf) -------- +pitch_interp = default_pitch_interp() +tsr_interp = default_tsr_interp() +tsrs = [tsr_interp(u) for u in wind_speeds] +pitches = [pitch_interp(u) for u in wind_speeds] + +# plot interpolated pitch and tsr data +fig, ax = plt.subplots(figsize=(8, 6)) +ax.scatter( + wind_speeds, + pitches, + s=40, + edgecolors="k", + label = "Interpolated Pitch [deg]" +) +ax.scatter( + wind_speeds, + tsrs, + s=40, + edgecolors="k", + label = "Interpolated Tip-Speed Ratio [-]" +) +# load and plot raw CSV data +ax.plot( + wind_table, + df["Pitch [deg]"].to_numpy(), + label = "Pitch [deg]" +) +ax.plot( + wind_table, + df["Tip Speed [m/s]"].to_numpy() / wind_table, + label = "Tip-Speed Ratio [-]" +) +ax.set_title("IEA 15MW: Fixed Bottom Trajectories", size = 18) +ax.set_xlabel("Wind Speed [m/s]", size = 16) +ax.tick_params(labelsize=14) +ax.legend(fontsize = 14) +plt.savefig(figdir / "example_6_pitch_tsr_interpolation.png", dpi=300) + +# -------- plot CT and CP values against one another and against IEA15MW from figure 3.1-C (https://docs.nrel.gov/docs/fy20osti/75698.pdf) ------- +# solve UMM-BEM though MITRotor - rotor averaged +pitches_rad = np.deg2rad(pitches) +bem_rotor_umm = default_bem_factory() +mit_rotor_umm_start = time.time() +pitches = np.deg2rad(pitch_interp(wind_speeds)) +tsrs = tsr_interp(wind_speeds) +yaws, tilts = np.zeros_like(pitches), np.zeros_like(pitches) +mit_sols_rotor_umm = bem_rotor_umm(pitch=pitches, tsr=tsrs, yaw=yaws, tilt=tilts) +mit_rotor_umm_end = time.time() +mit_Ct_rotor_umm = mit_sols_rotor_umm.Ct() +mit_Cp_rotor_umm = mit_sols_rotor_umm.Cp() +print("MITRotor UMM-BEM Rotor-Averaged: " + str(mit_rotor_umm_end - mit_rotor_umm_start) + " seconds") + +# solve UMM-BEM though MITRotor - annulus averaged +bem_annulus_umm = BEM( + rotor=IEA15MW(), + momentum_model=UnifiedMomentum(averaging="annulus"), + geometry=BEMGeometry(Nr=10, Ntheta=20), + tiploss_model=NoTipLoss(), + ) +mit_annulus_umm_start = time.time() +mit_sols_annulus_umm = bem_annulus_umm(pitch=pitches, tsr=tsrs, yaw=yaws, tilt=tilts) +mit_annulus_umm_end = time.time() +mit_Ct_annulus_umm = mit_sols_annulus_umm.Ct() +mit_Cp_annulus_umm = mit_sols_annulus_umm.Cp() +print("MITRotor UMM-BEM Annulus-Averaged: " + str(mit_annulus_umm_end - mit_annulus_umm_start) + " seconds") + +# solve UMM-BEM though MITRotor - sector averaged +# bem_sector_umm = BEM( +# rotor=IEA15MW(), +# momentum_model=UnifiedMomentum(averaging="sector"), +# geometry=BEMGeometry(Nr=10, Ntheta=20), +# tiploss_model=NoTipLoss(), +# ) +# mit_sector_umm_start = time.time() +# mit_sols_sector_umm = bem_sector_umm(pitch=pitches, tsr=tsrs, yaw=yaws, tilt=tilts) +# mit_sector_umm_end = time.time() +# mit_Ct_sector_umm = mit_sols_sector_umm.Ct() +# mit_Cp_sector_umm = mit_sols_sector_umm.Cp() +# print("MITRotor UMM-BEM Sector-Averaged: " + str(mit_sector_umm_end - mit_sector_umm_start) + " seconds") + +# solve UMM-BEM with LUT though MITRotor - annulus averaged +print("Making LUT!") +bem_annulus_umm_LUT = BEM( + rotor=IEA15MW(), + momentum_model=UnifiedMomentumLUT(averaging="annulus", cache_fn = Path("cache")/ "lut.csv"), + geometry=BEMGeometry(Nr=10, Ntheta=20), + tiploss_model=NoTipLoss(), +) +mit_annulus_umm_LUT_start = time.time() +mit_sols_annulus_umm_LUT = bem_annulus_umm_LUT(pitch=pitches, tsr=tsrs, yaw=yaws, tilt=tilts) +mit_annulus_umm_LUT_end = time.time() +mit_Ct_annulus_umm_LUT = mit_sols_annulus_umm_LUT.Ct() +mit_Cp_annulus_umm_LUT = mit_sols_annulus_umm_LUT.Cp() +print("MITRotor UMM-BEM LUT Annulus-Averaged: " + str(mit_annulus_umm_LUT_end - mit_annulus_umm_LUT_start) + " seconds") + +# solve FLORIS with UMM-BEM though MITRotor - rotor averaged +time_series = TimeSeries( + wind_speeds=wind_speeds, + wind_directions=wind_dirs, + turbulence_intensities=turbulence_intensity, +) +fmodel_rotor_umm = FlorisModel("defaults") +fmodel_rotor_umm.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) +fmodel_rotor_umm.set_operation_model(MITRotorTurbine()) # default bem_model uses rotor-averaging +floris_rotor_umm_start = time.time() +fmodel_rotor_umm.run() +floris_rotor_umm_end = time.time() +floris_Ct_rotor_umm = fmodel_rotor_umm.get_turbine_thrust_coefficients() +rotor_area = np.pi * bem_rotor_umm.rotor.R**2 +floris_power_rotor_umm = np.squeeze(fmodel_rotor_umm.get_turbine_powers()) +floris_Cp_rotor_umm = floris_power_rotor_umm / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) +print("FLORIS UMM-BEM Rotor-Averaged: " + str(floris_rotor_umm_end - floris_rotor_umm_start) + " seconds") + +# solve FLORIS with UMM-BEM though MITRotor - annulus averaged +fmodel_annulus_umm = FlorisModel("defaults") +fmodel_annulus_umm.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) +fmodel_annulus_umm.set_operation_model(MITRotorTurbine(bem_model = bem_annulus_umm)) # default bem_model uses rotor-averaging +floris_annulus_umm_start = time.time() +fmodel_annulus_umm.run() +floris_annulus_umm_end = time.time() +floris_Ct_annulus_umm = fmodel_annulus_umm.get_turbine_thrust_coefficients() +floris_power_annulus_umm = np.squeeze(fmodel_annulus_umm.get_turbine_powers()) +floris_Cp_annulus_umm = floris_power_annulus_umm / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) +print("FLORIS UMM-BEM Annulus-Averaged: " + str(floris_annulus_umm_end - floris_annulus_umm_start) + " seconds") + +# solve FLORIS with UMM-BEM with LUT though MITRotor - annulus averaged +fmodel_annulus_umm_LUT = FlorisModel("defaults") +fmodel_annulus_umm_LUT.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) +fmodel_annulus_umm_LUT.set_operation_model(MITRotorTurbine(bem_model = bem_annulus_umm_LUT)) # default bem_model uses rotor-averaging +floris_annulus_umm_LUT_start = time.time() +fmodel_annulus_umm_LUT.run() +floris_annulus_umm_LUT_end = time.time() +floris_Ct_annulus_umm_LUT = fmodel_annulus_umm_LUT.get_turbine_thrust_coefficients() +floris_power_annulus_umm_LUT = np.squeeze(fmodel_annulus_umm_LUT.get_turbine_powers()) +floris_Cp_annulus_umm_LUT = floris_power_annulus_umm_LUT / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) +print("FLORIS UMM-BEM LUT Annulus-Averaged: " + str(floris_annulus_umm_LUT_end - floris_annulus_umm_LUT_start) + " seconds") + +# Presentation-friendly typography +plt.rcParams.update({ + "font.size": 20, + "axes.titlesize": 20, + "axes.labelsize": 22, + "xtick.labelsize": 20, + "ytick.labelsize": 20, +}) + +# plot CT values against one another and against IEA15MW from figure 3.1-C (https://docs.nrel.gov/docs/fy20osti/75698.pdf) +fig, (ax0, ax1) = plt.subplots(figsize=(17, 9), ncols = 2, sharex = True, sharey = True) +fig.suptitle("IEA 15 MW FLORIS Interface Validation") +alpha = 0.6 +ax0.plot( + wind_table, + df["Thrust Coefficient [-]"].to_list(), + linewidth=6, + label="IEA15MW", + color='tab:orange', + linestyle = "solid", + alpha = alpha, + zorder = 1, +) + +ax0.plot( + wind_speeds, + mit_Ct_rotor_umm, + label="MITRotor UMM Rotor-Averaged", + linewidth=6, + color='tab:blue', + linestyle = "solid", + alpha = alpha, + zorder = 1, +) +ax0.plot( + wind_speeds, + mit_Ct_annulus_umm_LUT, + label="MITRotor UMM LUT Annulus-Averaged", + linewidth=6, + color='tab:red', + linestyle = "solid", + alpha = alpha, + zorder = 1, +) + +ax0.plot( + wind_speeds, + mit_Ct_annulus_umm, + label="MITRotor UMM Annulus-Averaged", + linewidth=6, + color='tab:green', + linestyle = "solid", + alpha = alpha, + zorder = 1, +) + +ax0.scatter( + wind_speeds, + floris_Ct_rotor_umm, + label="FLORIS-MITRotor UMM Rotor-Averaged", + color='tab:blue', + alpha = alpha, + marker = "o", + zorder = 2, + s = 80, +) +ax0.scatter( + wind_speeds, + floris_Ct_annulus_umm, + label="FLORIS-MITRotor UMM Annulus-Averaged", + color='tab:green', + alpha = alpha, + zorder = 2, + s = 80, + marker = "s" +) +ax0.scatter( + wind_speeds, + floris_Ct_annulus_umm_LUT, + label="FLORIS-MITRotor UMM LUT Annulus-Averaged", + color='tab:red', + alpha = alpha, + zorder = 2, + s = 80, + marker = "v" +) + +ax0.set_xlabel("Wind Speed [m/s]") +ax0.set_ylabel("$C_T$") +ax0.tick_params() +ax0.set_title("$C_T$") + +# plot Cp values against one another and against IEA15MW from figure 3.1-C (https://docs.nrel.gov/docs/fy20osti/75698.pdf) +ax1.plot( + wind_table, + df["Aero Power Coefficient [-]"].to_list(), + label="IEA15MW", + linewidth=6, + color='tab:orange', + linestyle = "solid", + alpha = alpha, + zorder = 1 +) +ax1.plot( + wind_speeds, + mit_Cp_rotor_umm, + label="MITRotor UMM Rotor-Averaged", + linewidth=6, + color='tab:blue', + linestyle = "solid", + alpha = alpha, + zorder = 1 +) +ax1.plot( + wind_speeds, + mit_Cp_annulus_umm_LUT, + label="MITRotor UMM LUT Annulus-Averaged", + linewidth=6, + color='tab:red', + linestyle = "solid", + alpha = alpha, + zorder = 1 +) +ax1.plot( + wind_speeds, + mit_Cp_annulus_umm, + label="MITRotor UMM Annulus-Averaged", + linewidth=6, + color='tab:green', + linestyle = "solid", + alpha = alpha, + zorder = 1 +) + +ax1.scatter( + wind_speeds, + floris_Cp_rotor_umm, + label="FLORIS-MITRotor UMM Rotor-Averaged", + color='tab:blue', + marker = "o", + alpha = alpha, + s = 80, + zorder = 2 +) +ax1.scatter( + wind_speeds, + floris_Cp_annulus_umm, + label="FLORIS-MITRotor UMM Annulus-Averaged", + color='tab:green', + marker = "s", + alpha = alpha, + s = 80, + zorder = 2 +) +ax1.scatter( + wind_speeds, + floris_Cp_annulus_umm_LUT, + label="FLORIS-MITRotor UMM LUT Annulus-Averaged", + color='tab:red', + marker = "v", + alpha = alpha, + s = 80, + zorder = 2 +) + +ax1.set_xlabel("Wind Speed [m/s]") +ax1.set_ylabel("$C_P$") +ax1.set_title("$C_P$") +ax1.legend( + fontsize=16, + loc="upper right", +) + +plt.savefig(figdir / "example_6_IEA15mw_CT_CP.png", dpi=300) diff --git a/examples/example_07_LUT_timing.py b/examples/example_07_LUT_timing.py new file mode 100644 index 0000000..988b814 --- /dev/null +++ b/examples/example_07_LUT_timing.py @@ -0,0 +1,128 @@ +import numpy as np +from pathlib import Path +from MITRotor.Momentum import UnifiedMomentumLUT, UnifiedMomentum +from MITRotor.TipLoss import NoTipLoss +from MITRotor import BEM, IEA15MW, BEMGeometry +import pandas as pd +import time + +# Floris imports +from floris import FlorisModel, TimeSeries +from MITRotor.FlorisInterface.FlorisInterface import MITRotorTurbine, default_bem_factory + +bem_rotor_umm = default_bem_factory() +bem_annulus_umm_LUT = BEM( + rotor=IEA15MW(), + momentum_model=UnifiedMomentumLUT(averaging="annulus", cache_fn = Path("cache")/ "lut.csv"), + geometry=BEMGeometry(Nr=10, Ntheta=20), + tiploss_model=NoTipLoss(), +) +rotor_area = np.pi * bem_rotor_umm.rotor.R**2 + +bem_rotor_times = [] +bem_annulus_LUT_times = [] +wind_speeds_all = [] +bem_rotor_values = [] +bem_annulus_LUT_values = [] +ns = [5 * i for i in range(1, 21)] +for n in ns: + print(f"{n} wind speeds") + wind_speeds = np.linspace(5, 20, n) + wind_dirs = np.full_like(wind_speeds, 270.0) + turbulence_intensity = np.zeros_like(wind_speeds) + wind_speeds_all.extend( + np.squeeze(wind_speeds) + ) + + time_series = TimeSeries( + wind_speeds=wind_speeds, + wind_directions=wind_dirs, + turbulence_intensities=turbulence_intensity, + ) + + fmodel_rotor_umm = FlorisModel("defaults") + fmodel_rotor_umm.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) + fmodel_rotor_umm.set_operation_model(MITRotorTurbine(bem_model = bem_rotor_umm)) # default bem_model uses rotor-averaging + floris_rotor_umm_start = time.time() + fmodel_rotor_umm.run() + floris_rotor_umm_end = time.time() + dt_rotor = floris_rotor_umm_end - floris_rotor_umm_start + print("FLORIS UMM-BEM Rotor-Averaged: " + str(dt_rotor) + " seconds") + bem_rotor_times.append(dt_rotor) + floris_Cp_rotor_umm = np.squeeze(fmodel_rotor_umm.get_turbine_powers()) / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) + bem_rotor_values.extend( + np.squeeze(floris_Cp_rotor_umm) + ) + + # solve FLORIS with UMM-BEM with LUT though MITRotor - annulus averaged + fmodel_annulus_umm_LUT = FlorisModel("defaults") + fmodel_annulus_umm_LUT.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) + fmodel_annulus_umm_LUT.set_operation_model(MITRotorTurbine(bem_model = bem_annulus_umm_LUT)) # default bem_model uses rotor-averaging + floris_annulus_umm_LUT_start = time.time() + fmodel_annulus_umm_LUT.run() + floris_annulus_umm_LUT_end = time.time() + dt_annulus_LUT = floris_annulus_umm_LUT_end - floris_annulus_umm_LUT_start + print("FLORIS UMM-BEM LUT Annulus-Averaged: " + str(dt_annulus_LUT) + " seconds") + bem_annulus_LUT_times.append(dt_annulus_LUT) + floris_Cp_annulus_umm_LUT = np.squeeze(fmodel_annulus_umm_LUT.get_turbine_powers()) / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) + bem_annulus_LUT_values.extend( + np.squeeze(floris_Cp_annulus_umm_LUT) + ) + +# make timing CSV +vectorized = True +rows = [] + +for n, dt in zip(ns, bem_rotor_times): + rows.append({ + "n_wind_speeds": n, + "runtime_seconds": dt, + "model": "rotor_umm", + "vectorized": vectorized + }) + +for n, dt in zip(ns, bem_annulus_LUT_times): + rows.append({ + "n_wind_speeds": n, + "runtime_seconds": dt, + "model": "annulus_lut", + "vectorized": vectorized + }) + +df = pd.DataFrame(rows) + +csv_path = Path("cache")/ "timing_results.csv" + +if csv_path.exists(): + df.to_csv(csv_path, mode="a", header=False, index=False) +else: + df.to_csv(csv_path, index=False) + +# make values CSV +rows = [] +# Rotor +for wind, val in zip(wind_speeds_all, bem_rotor_values): + rows.append({ + "wind_speed": wind, + "power": val, + "model": "rotor_umm", + "vectorized": vectorized + }) + +# Annulus LUT +for wind, val in zip(wind_speeds_all, bem_annulus_LUT_values): + rows.append({ + "wind_speed": wind, + "power": val, + "model": "annulus_lut", + "vectorized": vectorized + }) + +df = pd.DataFrame(rows) + +csv_path = Path("cache") / "value_results.csv" + +if csv_path.exists(): + df.to_csv(csv_path, mode="a", header=False, index=False) +else: + df.to_csv(csv_path, index=False) diff --git a/examples/example_timing.py b/examples/example_timing.py new file mode 100644 index 0000000..169e3d7 --- /dev/null +++ b/examples/example_timing.py @@ -0,0 +1,129 @@ +import numpy as np +from pathlib import Path +from MITRotor.Momentum import UnifiedMomentumLUT, UnifiedMomentum +from MITRotor.TipLoss import NoTipLoss +from MITRotor import BEM, IEA15MW, BEMGeometry +import pandas as pd +import time + +# Floris imports +from floris import FlorisModel, TimeSeries +from MITRotor.FlorisInterface.FlorisInterface import MITRotorTurbine, default_bem_factory + +bem_rotor_umm = default_bem_factory() +bem_annulus_umm_LUT = BEM( + rotor=IEA15MW(), + momentum_model=UnifiedMomentumLUT(averaging="annulus", cache_fn = Path("cache")/ "lut.csv"), + geometry=BEMGeometry(Nr=10, Ntheta=20), + tiploss_model=NoTipLoss(), +) +rotor_area = np.pi * bem_rotor_umm.rotor.R**2 + +bem_rotor_times = [] +bem_annulus_LUT_times = [] +wind_speeds_all = [] +bem_rotor_values = [] +bem_annulus_LUT_values = [] +ns = [5 * i for i in range(1, 21)] +for n in ns: + print(f"{n} wind speeds") + wind_speeds = np.linspace(5, 20, n) + wind_dirs = np.full_like(wind_speeds, 270.0) + turbulence_intensity = np.zeros_like(wind_speeds) + wind_speeds_all.extend( + np.squeeze(wind_speeds) + ) + + time_series = TimeSeries( + wind_speeds=wind_speeds, + wind_directions=wind_dirs, + turbulence_intensities=turbulence_intensity, + ) + + fmodel_rotor_umm = FlorisModel("defaults") + fmodel_rotor_umm.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) + fmodel_rotor_umm.set_operation_model(MITRotorTurbine(bem_model = bem_rotor_umm)) # default bem_model uses rotor-averaging + floris_rotor_umm_start = time.time() + fmodel_rotor_umm.run() + floris_rotor_umm_end = time.time() + dt_rotor = floris_rotor_umm_end - floris_rotor_umm_start + print("FLORIS UMM-BEM Rotor-Averaged: " + str(dt_rotor) + " seconds") + bem_rotor_times.append(dt_rotor) + floris_Cp_rotor_umm = np.squeeze(fmodel_rotor_umm.get_turbine_powers()) / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) + bem_rotor_values.extend( + np.squeeze(floris_Cp_rotor_umm) + ) + + # solve FLORIS with UMM-BEM with LUT though MITRotor - annulus averaged + fmodel_annulus_umm_LUT = FlorisModel("defaults") + fmodel_annulus_umm_LUT.set(layout_x = [0.0], layout_y = [0.0], wind_data = time_series) + fmodel_annulus_umm_LUT.set_operation_model(MITRotorTurbine(bem_model = bem_annulus_umm_LUT)) # default bem_model uses rotor-averaging + floris_annulus_umm_LUT_start = time.time() + fmodel_annulus_umm_LUT.run() + floris_annulus_umm_LUT_end = time.time() + dt_annulus_LUT = floris_annulus_umm_LUT_end - floris_annulus_umm_LUT_start + print("FLORIS UMM-BEM LUT Annulus-Averaged: " + str(dt_annulus_LUT) + " seconds") + bem_annulus_LUT_times.append(dt_annulus_LUT) + floris_Cp_annulus_umm_LUT = np.squeeze(fmodel_annulus_umm_LUT.get_turbine_powers()) / (0.5 * 1.225 * rotor_area * (wind_speeds)**3) + bem_annulus_LUT_values.extend( + np.squeeze(floris_Cp_annulus_umm_LUT) + ) + +# make timing CSV +vectorized = False +rows = [] + +for n, dt in zip(ns, bem_rotor_times): + rows.append({ + "n_wind_speeds": n, + "runtime_seconds": dt, + "model": "rotor_umm", + "vectorized": vectorized + }) + +for n, dt in zip(ns, bem_annulus_LUT_times): + rows.append({ + "n_wind_speeds": n, + "runtime_seconds": dt, + "model": "annulus_lut", + "vectorized": vectorized + }) + +df = pd.DataFrame(rows) + +csv_path = Path("cache")/ "timing_results.csv" + +if csv_path.exists(): + df.to_csv(csv_path, mode="a", header=False, index=False) +else: + df.to_csv(csv_path, index=False) + +# make values CSV +rows = [] +# Rotor +for wind, val in zip(wind_speeds_all, bem_rotor_values): + rows.append({ + "wind_speed": wind, + "power": val, + "model": "rotor_umm", + "vectorized": vectorized + }) + +# Annulus LUT +for wind, val in zip(wind_speeds_all, bem_annulus_LUT_values): + rows.append({ + "wind_speed": wind, + "power": val, + "model": "annulus_lut", + "vectorized": vectorized + }) + +df = pd.DataFrame(rows) + +csv_path = Path("cache") / "value_results.csv" + +if csv_path.exists(): + df.to_csv(csv_path, mode="a", header=False, index=False) +else: + df.to_csv(csv_path, index=False) + diff --git a/examples/example_timing_plots.py b/examples/example_timing_plots.py new file mode 100644 index 0000000..0bc9835 --- /dev/null +++ b/examples/example_timing_plots.py @@ -0,0 +1,96 @@ +import pandas as pd +import matplotlib.pyplot as plt +from pathlib import Path + +# ---- Load data ---- +figdir = Path("fig") +time_df = pd.read_csv(Path("cache")/ "timing_results.csv") +value_df = pd.read_csv(Path("cache")/ "value_results.csv") + +#------------------------------------ +# Timing plot +#------------------------------------ +df_avg = ( + time_df.groupby(["n_wind_speeds", "model", "vectorized"], as_index=False) + .agg( + runtime_mean=("runtime_seconds", "mean"), + runtime_std=("runtime_seconds", "std"), + ) +) + +# ---- Create label column for plotting ---- +def make_label(row): + base = { + "rotor_umm": "UMM Rotor-Averaged", + "annulus_lut": "UMM LUT Annulus-Averaged", + }[row["model"]] + + if row["vectorized"]: + return base + " (Vectorized)" + else: + return base + +df_avg["label"] = df_avg.apply(make_label, axis=1) + +label_order = [ + "UMM Rotor-Averaged", + "UMM LUT Annulus-Averaged", + "UMM Rotor-Averaged (Vectorized)", + "UMM LUT Annulus-Averaged (Vectorized)", +] + +# ---- Plot ---- +plt.figure(figsize=(6, 4)) + +for label in label_order: + group = df_avg[df_avg["label"] == label] + group = group.sort_values("n_wind_speeds") + + plt.errorbar( + group["n_wind_speeds"], + group["runtime_mean"], + yerr=group["runtime_std"], + marker="o", + capsize=3, + label=label, + linewidth = 2, + ) + # plt.yscale("log") + +plt.xlabel("Number of Wind Speeds Run\n(evenly-spaced between 5-20 m/s)") +plt.ylabel("Runtime (seconds)") +plt.title("Runtime vs Number of Wind Speeds") +plt.legend() +plt.grid(True) +plt.tight_layout() +plt.savefig(figdir / "example_timings.png", dpi=300) + +#------------------------------------ +# Value plot +#------------------------------------ +plt.figure(figsize=(6, 4)) + +value_df["label"] = value_df.apply(make_label, axis=1) + +for label in label_order: + group = value_df[value_df["label"] == label] + group = group.sort_values("wind_speed") + is_vectorized = group["vectorized"].iloc[0] + linestyle = "dashed" if is_vectorized else "solid" + zorder = 2 if is_vectorized else 1 + plt.plot( + group["wind_speed"], + group["power"], + linewidth = 3, + label=label, + linestyle = linestyle, + zorder = zorder, + ) + +plt.xlabel("Wind Speed [m/s]") +plt.ylabel("Coefficent of Power ($C_P$)") +plt.title("$C_P$ vs Wind Speed") +plt.legend() +plt.grid(True) +plt.tight_layout() +plt.savefig(figdir / "example_values.png", dpi=300) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index d39f320..2fb0d6a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -147,7 +147,7 @@ version = "25.4.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, @@ -519,6 +519,24 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", examples = "platform_system == \"Windows\""} +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + [[package]] name = "comm" version = "0.2.3" @@ -540,8 +558,8 @@ version = "1.3.2" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.10" -groups = ["examples"] -markers = "python_version < \"3.14\"" +groups = ["main", "examples"] +markers = "python_version == \"3.10\"" files = [ {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, @@ -618,8 +636,8 @@ version = "1.3.3" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.11" -groups = ["examples"] -markers = "python_version >= \"3.14\"" +groups = ["main", "examples"] +markers = "python_version >= \"3.11\"" files = [ {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, @@ -711,7 +729,7 @@ version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -785,6 +803,22 @@ files = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] +[[package]] +name = "dill" +version = "0.4.1" +description = "serialize all of Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -834,13 +868,45 @@ files = [ [package.extras] devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] +[[package]] +name = "floris" +version = "4.6" +description = "A controls-oriented engineering wake model." +optional = false +python-versions = ">=3.10, <3.15" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +attrs = "*" +coloredlogs = ">=15.0,<16.0" +matplotlib = ">=3.0,<4.0" +numexpr = ">=2.0,<3.0" +numpy = ">=2.0,<3.0" +pandas = ">=2.0,<3.0" +pathos = ">=0.3,<1.0" +pyyaml = ">=6.0,<7.0" +scipy = ">=1.1,<2.0" +shapely = ">=2.0,<3.0" + +[package.extras] +develop = ["isort (>=5,<8)", "pre-commit (>=4.0,<5.0)", "pytest (>=8,<10)", "pytest-benchmark (>=5.1,<6.0)", "ruff (>=0.9,<1.0)"] +docs = ["bokeh (>=3.7,<4.0)", "jupyter-book (>=1.0,<2.0)", "ruamel.yaml (>=0.18.0,<0.19.0)", "sphinx-autodoc-typehints (>=2,<4)", "sphinx-book-theme (>=1.0,<2.0)", "sphinxcontrib-autoyaml (>=1.0,<2.0)", "sphinxcontrib.mermaid (>=1.0,<2.0)"] + +[package.source] +type = "git" +url = "https://github.com/misi9170/floris.git" +reference = "feature/user-def-op-mod" +resolved_reference = "c21ebc167354ef1b768d12ce08eb2ae2613bb055" + [[package]] name = "fonttools" version = "4.61.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.10" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24"}, {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958"}, @@ -993,6 +1059,21 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + [[package]] name = "idna" version = "3.11" @@ -1517,7 +1598,7 @@ version = "1.4.9" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, @@ -1846,7 +1927,7 @@ version = "3.10.8" description = "Python plotting package" optional = false python-versions = ">=3.10" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7"}, {file = "matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656"}, @@ -1952,6 +2033,35 @@ files = [ [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.11\""} +[[package]] +name = "multiprocess" +version = "0.70.19" +description = "better multiprocessing and multithreading in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multiprocess-0.70.19-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:02e5c35d7d6cd2bdc89c1858867f7bde4012837411023a4696c148c1bdd7c80e"}, + {file = "multiprocess-0.70.19-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79576c02d1207ec405b00cabf2c643c36070800cca433860e14539df7818b2aa"}, + {file = "multiprocess-0.70.19-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6b6d78d43a03b68014ca1f0b7937d965393a670c5de7c29026beb2258f2f896"}, + {file = "multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7"}, + {file = "multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e"}, + {file = "multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45"}, + {file = "multiprocess-0.70.19-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:e5e7dc3e3e1732e88c07aaec17eeb9917f9ed1107d9e60d5ab985cdc14bac43a"}, + {file = "multiprocess-0.70.19-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:e6c0674d34b8adac22533f6786576b3de4e396aaeda9e0c15378af9b8ada2702"}, + {file = "multiprocess-0.70.19-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d6db91ca6391eebc139c352f34578cea382df6bfa03d3b4146ed12b18b01cc14"}, + {file = "multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87"}, + {file = "multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c"}, + {file = "multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28"}, + {file = "multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952"}, + {file = "multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f"}, + {file = "multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5"}, + {file = "multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897"}, +] + +[package.dependencies] +dill = ">=0.4.1" + [[package]] name = "mypy" version = "1.19.1" @@ -2162,6 +2272,76 @@ jupyter-server = ">=1.8,<3" [package.extras] test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] +[[package]] +name = "numexpr" +version = "2.14.1" +description = "Fast numerical expression evaluator for NumPy" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "numexpr-2.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0fab3fd06a04f6b86102552b26aa5d85e20ac7d8296c15764c726eeabae6cc8"}, + {file = "numexpr-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:64ae5dfd62d74a3ef82fe0b37f80527247f3626171ad82025900f46ffca4b39a"}, + {file = "numexpr-2.14.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:955c92b064f9074d2970cf3138f5e3b965be673b82024962ed526f39bc25a920"}, + {file = "numexpr-2.14.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75440c54fc01e130396650fdf307aa9d41a67dc06ddbfb288971b591c13a395b"}, + {file = "numexpr-2.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dde9fa47ed319e1e1728940a539df3cb78326b7754bc7c6ab3152afc91808f9b"}, + {file = "numexpr-2.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76db0bc6267e591ab9c4df405ffb533598e4c88239db7338d11ae9e4b368a85a"}, + {file = "numexpr-2.14.1-cp310-cp310-win32.whl", hash = "sha256:0d1dcbdc4d0374c0d523cee2f94f06b001623cbc1fd163612841017a3495427c"}, + {file = "numexpr-2.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:823cd82c8e7937981339f634e7a9c6a92cb2d0b9d0a5cf627a5e394fffc05377"}, + {file = "numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34"}, + {file = "numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f"}, + {file = "numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4"}, + {file = "numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6"}, + {file = "numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb"}, + {file = "numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a"}, + {file = "numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed"}, + {file = "numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7"}, + {file = "numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430"}, + {file = "numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659"}, + {file = "numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1"}, + {file = "numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083"}, + {file = "numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48"}, + {file = "numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b"}, + {file = "numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07"}, + {file = "numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f"}, + {file = "numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222"}, + {file = "numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6"}, + {file = "numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30"}, + {file = "numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c"}, + {file = "numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5"}, + {file = "numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421"}, + {file = "numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b"}, + {file = "numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb"}, + {file = "numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b"}, + {file = "numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73"}, + {file = "numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc"}, + {file = "numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9"}, + {file = "numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e"}, + {file = "numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19"}, + {file = "numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611"}, + {file = "numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09"}, + {file = "numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21"}, + {file = "numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082"}, + {file = "numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574"}, + {file = "numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de"}, + {file = "numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8"}, + {file = "numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c"}, + {file = "numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3"}, + {file = "numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2"}, + {file = "numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5"}, + {file = "numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a"}, + {file = "numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091"}, + {file = "numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73"}, + {file = "numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3"}, + {file = "numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e"}, + {file = "numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e"}, + {file = "numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392"}, + {file = "numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b"}, +] + +[package.dependencies] +numpy = ">=1.23.0" + [[package]] name = "numpy" version = "2.2.6" @@ -2169,7 +2349,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["main", "examples"] -markers = "python_version < \"3.14\"" +markers = "python_version == \"3.10\"" files = [ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, @@ -2235,7 +2415,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" groups = ["main", "examples"] -markers = "python_version >= \"3.14\"" +markers = "python_version >= \"3.11\"" files = [ {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, @@ -2330,12 +2510,112 @@ version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev", "examples"] +groups = ["main", "dev", "examples"] files = [ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] +[[package]] +name = "pandas" +version = "2.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + [[package]] name = "pandocfilters" version = "1.5.1" @@ -2364,6 +2644,24 @@ files = [ qa = ["flake8 (==5.0.4)", "types-setuptools (==67.2.0.1)", "zuban (==0.5.1)"] testing = ["docopt", "pytest"] +[[package]] +name = "pathos" +version = "0.3.5" +description = "parallel graph management and execution in heterogeneous computing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pathos-0.3.5-py3-none-any.whl", hash = "sha256:c95b04103c40a16c08db69cd4b5c52624d55208beadf1348681edece809ec4f8"}, + {file = "pathos-0.3.5.tar.gz", hash = "sha256:8fe041b8545c5d3880a038f866022bdebf935e5cf68f56ed3407cb7e65193a61"}, +] + +[package.dependencies] +dill = ">=0.4.1" +multiprocess = ">=0.70.19" +pox = ">=0.3.7" +ppft = ">=1.7.8" + [[package]] name = "pathspec" version = "1.0.4" @@ -2404,7 +2702,7 @@ version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, @@ -2572,6 +2870,33 @@ timezone = ["backports.zoneinfo ; python_version < \"3.9\"", "tzdata ; platform_ xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] +[[package]] +name = "pox" +version = "0.3.7" +description = "utilities for filesystem exploration and automated builds" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pox-0.3.7-py3-none-any.whl", hash = "sha256:82a495249d13371314c1a5b5626a115e067ef5215d49530bf5efa37fbc25b56a"}, + {file = "pox-0.3.7.tar.gz", hash = "sha256:0652f6f2103fe6d4ba638beb6fa8d3e8a68fd44bcb63315c614118515bcc3afb"}, +] + +[[package]] +name = "ppft" +version = "1.7.8" +description = "distributed and parallel Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ppft-1.7.8-py3-none-any.whl", hash = "sha256:d3e0e395215b14afc3dd5adfc032ccecfda2d4ed50dc7ded076cd1d215442843"}, + {file = "ppft-1.7.8.tar.gz", hash = "sha256:5f696d4f397ae9b0af39b1faffb31957c51dfbc5a3815856472d4f4e872937ee"}, +] + +[package.extras] +dill = ["dill (>=0.4.1)"] + [[package]] name = "prometheus-client" version = "0.24.1" @@ -2701,7 +3026,7 @@ version = "3.3.2" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" -groups = ["examples"] +groups = ["main", "examples"] files = [ {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, @@ -2710,6 +3035,22 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pytest" version = "7.4.4" @@ -2739,7 +3080,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev", "examples"] +groups = ["main", "dev", "examples"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2763,6 +3104,18 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec ; implementation_name != \"pypy\"", "mypy", "orjson ; implementation_name != \"pypy\"", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + [[package]] name = "pywinpty" version = "3.0.3" @@ -3221,7 +3574,7 @@ description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "python_version < \"3.14\"" +markers = "python_version == \"3.10\"" files = [ {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, @@ -3286,7 +3639,7 @@ description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.14\"" +markers = "python_version >= \"3.11\"" files = [ {file = "scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec"}, {file = "scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696"}, @@ -3396,13 +3749,87 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] +[[package]] +name = "shapely" +version = "2.1.2" +description = "Manipulation and analysis of geometric objects" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f"}, + {file = "shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea"}, + {file = "shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f"}, + {file = "shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142"}, + {file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4"}, + {file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0"}, + {file = "shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e"}, + {file = "shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f"}, + {file = "shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618"}, + {file = "shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d"}, + {file = "shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09"}, + {file = "shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26"}, + {file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7"}, + {file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2"}, + {file = "shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6"}, + {file = "shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc"}, + {file = "shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94"}, + {file = "shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359"}, + {file = "shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3"}, + {file = "shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b"}, + {file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc"}, + {file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d"}, + {file = "shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454"}, + {file = "shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179"}, + {file = "shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8"}, + {file = "shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a"}, + {file = "shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e"}, + {file = "shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6"}, + {file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af"}, + {file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd"}, + {file = "shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350"}, + {file = "shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715"}, + {file = "shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40"}, + {file = "shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b"}, + {file = "shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801"}, + {file = "shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0"}, + {file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c"}, + {file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99"}, + {file = "shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf"}, + {file = "shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c"}, + {file = "shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223"}, + {file = "shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c"}, + {file = "shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df"}, + {file = "shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf"}, + {file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4"}, + {file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc"}, + {file = "shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566"}, + {file = "shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c"}, + {file = "shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a"}, + {file = "shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076"}, + {file = "shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1"}, + {file = "shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0"}, + {file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26"}, + {file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0"}, + {file = "shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735"}, + {file = "shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9"}, + {file = "shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9"}, +] + +[package.dependencies] +numpy = ">=1.21" + +[package.extras] +docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] +test = ["pytest", "pytest-cov", "scipy-doctest"] + [[package]] name = "six" version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev", "examples"] +groups = ["main", "dev", "examples"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -3629,7 +4056,7 @@ version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, @@ -3763,5 +4190,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = ">=3.10,<4.0" -content-hash = "fbfaf93683678bf7892ad47c20c98b358472a8122c7e2c210a885cb80c79fbed" +python-versions = ">=3.10, <3.15" +content-hash = "0577e837066a12dcfc7e702c1fac22c2f7ffef58e19c837dc1fc791637a2b19e" diff --git a/pyproject.toml b/pyproject.toml index 239968e..8eb55b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,12 @@ readme = "README.md" packages = [{ include = "MITRotor" }] [tool.poetry.dependencies] -python = ">=3.10,<4.0" +python = ">=3.10, <3.15" numpy = "^2.2.1" scipy = ">=1.6" unified-momentum-model = {git = "https://github.com/Howland-Lab/Unified-Momentum-Model.git"} pyyaml = "^6.0.1" +floris = {git = "https://github.com/misi9170/floris.git", branch = "feature/user-def-op-mod"} [tool.poetry.group.dev.dependencies] black = { extras = ["jupyter"], version = "^23.9.1" } @@ -30,14 +31,14 @@ matplotlib = "^3.7.3" polars = "^0.19.2" tqdm = "^4.66.1" - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" - [tool.black] line-length = 120 [tool.ruff] line-length = 120 + +include = ["MITRotor/FlorisInterface/*.csv"] diff --git a/tests/test_bem.py b/tests/test_bem.py index b721efd..e58e7f4 100644 --- a/tests/test_bem.py +++ b/tests/test_bem.py @@ -1,4 +1,4 @@ -from MITRotor import BEM, IEA15MW, UnifiedMomentum, BEMGeometry +from MITRotor import BEM, IEA15MW, UnifiedMomentum, UnifiedMomentumLUT, BEMGeometry from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw import numpy as np from pytest import approx @@ -6,7 +6,6 @@ def test_IEA15MW(): IEA15MW() - def test_BEM_initialise(): rotor = IEA15MW() BEM(rotor=rotor) @@ -102,4 +101,72 @@ def test_model_yaw_tilt_rotor_phase(): # tilt and yaw should be offset by 90 degrees assert np.isclose(np.abs(theta_mesh[yaw_max_idx] - theta_mesh[tilt_max_idx]), np.pi / 2, atol = deg_atol) # yaw and evenly split yaw/tilt should be offset by 45 degrees - assert np.isclose(np.abs(theta_mesh[yaw_max_idx] - theta_mesh[yaw_and_tilt_max_idx]), np.pi / 4, atol = deg_atol) \ No newline at end of file + assert np.isclose(np.abs(theta_mesh[yaw_max_idx] - theta_mesh[yaw_and_tilt_max_idx]), np.pi / 4, atol = deg_atol) + +def _expected_shape(grid, Np, Nr, Ntheta): + if grid == "rotor": + return () if Np == 0 else (Np,) + elif grid == "annulus": + return (Nr,) if Np == 0 else (Np, Nr) + elif grid == "sector": + return (Nr, Ntheta) if Np == 0 else (Np, Nr, Ntheta) + else: + raise ValueError(grid) + + +def test_BEM_dimensionality(): + Nr, Ntheta = 20, 40 + rotor = IEA15MW() + + bems = [ + BEM(rotor=rotor, + momentum_model=UnifiedMomentum(averaging="rotor"), + geometry=BEMGeometry(Nr=Nr, Ntheta=Ntheta)), + + BEM(rotor=rotor, + momentum_model=UnifiedMomentumLUT(averaging="annulus"), + geometry=BEMGeometry(Nr=Nr, Ntheta=Ntheta)), + + BEM(rotor=rotor, + momentum_model=UnifiedMomentumLUT(averaging="sector"), + geometry=BEMGeometry(Nr=Nr, Ntheta=Ntheta)), + ] + + # --- inputs --- + pitch_s, tsr_s, yaw_s, tilt_s = 0.0, 7.0, 0.0, 0.0 + + pitch_v = np.array([0.0, 0.1, 0.2]) + tsr_v = np.array([6.0, 7.0, 8.0]) + yaw_v = np.array([0.0, 0.1, 0.2]) + tilt_v = np.array([0.0, -0.1, -0.2]) + + cases = [ + (pitch_s, tsr_s, yaw_s, tilt_s, 0), + (pitch_v, tsr_v, yaw_v, tilt_v, 3), + ] + + fields = ["Cp", "Cp_corr", "Ct", "a", "aoa"] + + for pitch, tsr, yaw, tilt, Np in cases: + for bem in bems: + sol = bem(pitch, tsr, yaw=yaw, tilt=tilt) + + # ========================= + # 1. u4, w4 dimensionality + # ========================= + u4 = np.asarray(sol.u4) + w4 = np.asarray(sol.w4) + + expected = () if Np == 0 else (Np,) + assert u4.shape == expected + assert w4.shape == expected + + # ========================= + # 2. Shape checks for fields + # ========================= + for grid in ["rotor", "annulus", "sector"]: + expected_shape = _expected_shape(grid, Np, Nr, Ntheta) + + for name in fields: + val = np.asarray(getattr(sol, name)(grid=grid)) + assert val.shape == expected_shape, f"{name}, {grid}" diff --git a/tests/test_floris_interface.py b/tests/test_floris_interface.py new file mode 100644 index 0000000..ad109ad --- /dev/null +++ b/tests/test_floris_interface.py @@ -0,0 +1,115 @@ +import os +import numpy as np +import pytest +import polars as pl +from numpy.testing import assert_almost_equal, assert_allclose +from floris import FlorisModel, TimeSeries +from MITRotor.FlorisInterface.FlorisInterface import MITRotorTurbine, default_bem_factory, default_pitch_interp, default_tsr_interp + + +def test_pitch_tsr_interpolation(): + # create default interpolators + tsr_interp = default_tsr_interp() + pitch_interp = default_pitch_interp() + + # load raw CSV data + module_dir = os.path.dirname(__file__) # tests/ + validation_csv = os.path.abspath(os.path.join(module_dir, "..", "MITRotor", "FlorisInterface", "IEA_15mw_rotor.csv")) + df = pl.read_csv(validation_csv) + wind_data = df["Wind [m/s]"].to_numpy() + pitch_data = df["Pitch [deg]"].to_numpy() + tip_speed_data = df["Tip Speed [m/s]"].to_numpy() + tsr_data = tip_speed_data / wind_data + + # interpolator reproduces raw data + assert_allclose(tsr_interp(wind_data), tsr_data, rtol=1e-12, atol=1e-12) + assert_allclose(pitch_interp(wind_data), pitch_data, rtol=1e-12, atol=1e-12) + + # reasonable values + x_interp_vals = np.linspace(0.0, 25.0, 100) + tsr_interp_vals = tsr_interp(x_interp_vals) + pitch_interp_vals = pitch_interp(x_interp_vals) + assert np.all(np.isfinite(tsr_interp_vals)) + assert np.all(np.isfinite(pitch_interp_vals)) + assert np.all(tsr_interp_vals > 0.0) + assert np.all(pitch_interp_vals >= -10.0) # deg, loose bound + assert np.all(pitch_interp_vals <= 40.0) + + +# compute MITRotor BEM outputs directly +def compute_mitrotor_cp_ct_a(wind_speeds, yaw_deg = 0.0, tilt_deg = 0.0): + bem_model = default_bem_factory() # default BEM (IEA15MW) used in floris interface + pitch_interp = default_pitch_interp() # IEA15MW pitch curve + tsr_interp = default_tsr_interp() # IEA15MW tsr curve + + n = len(wind_speeds) + Ct = np.empty(n) + a = np.empty(n) + for i, ws in enumerate(wind_speeds): + pitch = np.deg2rad(pitch_interp(ws)) + tsr = tsr_interp(ws) + sol = bem_model(pitch, tsr, yaw=np.deg2rad(yaw_deg), tilt = np.deg2rad(tilt_deg)) + Ct[i] = sol.Ct() + a[i] = sol.a() + + return Ct, a + +@pytest.mark.parametrize("n_turbines", [1, 2]) +def test_mitrotor_floris_wind_speeds(n_turbines): + wind_speeds = np.array([6.0, 8.0, 10.0, 12.0]) + wind_dirs = np.full_like(wind_speeds, 270.0) + turbulence_intensity = np.zeros_like(wind_speeds) + + time_series = TimeSeries( + wind_speeds=wind_speeds, + wind_directions=wind_dirs, + turbulence_intensities=turbulence_intensity, + ) + + layout_x = np.linspace(0.0, 500.0 * (n_turbines - 1), n_turbines) + layout_y = np.zeros_like(layout_x) + + fmodel = FlorisModel("defaults") + fmodel.set(layout_x=layout_x, layout_y=layout_y, wind_data=time_series) + fmodel.set_operation_model(MITRotorTurbine()) + + fmodel.run() + + floris_Ct = fmodel.get_turbine_thrust_coefficients() + floris_a = fmodel.get_turbine_axial_induction_factors() + + mit_Ct, mit_a = compute_mitrotor_cp_ct_a(wind_speeds) + + # First turbine matches MITRotor BEM (<1% error) + assert_almost_equal(floris_Ct[:, 0], mit_Ct, decimal=2) + assert_almost_equal(floris_a[:, 0], mit_a, decimal=2) + + if n_turbines > 1: + # first turbine produces more power than the second (wake effects) + floris_power = fmodel.get_turbine_powers() + assert np.all(floris_power[:, 0] > floris_power[:, 1]) + + # yawing the first turbine decreases first turbine power and increases second turbine power + fmodel = FlorisModel("defaults") + yaw_angles = np.tile(np.array([5.0, 0.0]), (4,1)) + fmodel.set(layout_x=layout_x, layout_y=layout_y, wind_data=time_series, yaw_angles = yaw_angles) + fmodel.set_operation_model(MITRotorTurbine()) + fmodel.run() + yawed_floris_power = fmodel.get_turbine_powers() + assert np.all(floris_power[:, 0] > yawed_floris_power[:, 0]) + assert np.all(floris_power[:, 1] < yawed_floris_power[:, 1]) + + # TODO: uncomment if tilt is able to be set for Floris + # tilting the first turbine decreases first turbine power and increases second turbine power + # fmodel = FlorisModel("defaults") + # tilt_angles = np.tile(np.array([5.0, 0.0]), (4,1)) + # fmodel.set(layout_x=layout_x, layout_y=layout_y, wind_data=time_series, tilt_angles = tilt_angles) + # fmodel.set_operation_model(MITRotorTurbine()) + # fmodel.run() + # tilted_floris_power = fmodel.get_turbine_powers() + # assert np.all(floris_power[:, 0] > tilted_floris_power[:, 0]) + # assert np.all(floris_power[:, 1] < tilted_floris_power[:, 1]) + + # # yawing and tilting turbines is equivalent + # assert_almost_equal(yawed_floris_power[:, 0], tilted_floris_power[:, 0], decimal=4) + # assert_almost_equal(yawed_floris_power[:, 1], tilted_floris_power[:, 1], decimal=4)