diff --git a/diagnostic_interface/canvas/realtime_canvas.py b/diagnostic_interface/canvas/realtime_canvas.py index 705e664..0529681 100644 --- a/diagnostic_interface/canvas/realtime_canvas.py +++ b/diagnostic_interface/canvas/realtime_canvas.py @@ -6,7 +6,6 @@ from diagnostic_interface import settings from PyQt5.QtWidgets import QMessageBox import mplcursors -from datetime import timezone from dateutil import tz diff --git a/diagnostic_interface/widgets/realtime_map_widget.py b/diagnostic_interface/widgets/realtime_map_widget.py index fdf407a..e9acdc2 100644 --- a/diagnostic_interface/widgets/realtime_map_widget.py +++ b/diagnostic_interface/widgets/realtime_map_widget.py @@ -45,7 +45,8 @@ def update_map(self, vertex_values: NDArray[float], latitudes: NDArray, longitud touchZoom=False, ) - get_coord = lambda x: [latitudes[x], longitudes[x]] + def get_coord(x): + return [latitudes[x], longitudes[x]] for i, (latitude, longitude, vertex_value) in enumerate(zip(latitudes, longitudes, vertex_values)): color = mcolors.to_hex(cmap(norm(vertex_value))) diff --git a/micro_strategy/__init__.py b/micro_strategy/__init__.py index a22a565..f9b187c 100644 --- a/micro_strategy/__init__.py +++ b/micro_strategy/__init__.py @@ -3,3 +3,6 @@ OptimizedSpeedsPath = Path(__file__).parent / "optimization_results" / "optimized_speeds_filtered.npy" +__all__ = [ + "run_micro_simulation" +] \ No newline at end of file diff --git a/prescriptive_interface/prescriptive_ui.py b/prescriptive_interface/prescriptive_ui.py index 9c4fd4d..b714ad9 100644 --- a/prescriptive_interface/prescriptive_ui.py +++ b/prescriptive_interface/prescriptive_ui.py @@ -11,18 +11,18 @@ from diagnostic_interface.widgets import SplashOverlay from simulation.cmd.run_simulation import get_default_settings from simulation.config import SimulationReturnType, SimulationHyperparametersConfig, InitialConditions -from pathlib import Path from PyQt5.QtCore import QTimer from PyQt5.QtCore import Qt from qt_material import apply_stylesheet -config_dir = Path(__file__).parent.parent / "simulation" / "config" from pathlib import Path from prescriptive_interface import (SimulationTab, OptimizationTab, HtmlViewerTab, SimulationSettingsDict, OptimizationThread, SimulationThread, MutableInitialConditions, InitialConditionsDialog) +config_dir = Path(__file__).parent.parent / "simulation" / "config" + my_speeds_dir = Path("prescriptive_interface/speeds_directory") my_speeds_dir.mkdir(parents=True, exist_ok=True) # Create it if it doesn't exist diff --git a/pyproject.toml b/pyproject.toml index 9e7c041..df87d78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dependencies = [ "tomli>=2.2.1", "tomli-w>=1.2.0", "qt-material>=2.17", - "ubc-solar-physics==1.8.2", + "ubc-solar-physics==1.9.1", ] [project.urls] diff --git a/simulation/config/BrightSide.toml b/simulation/config/BrightSide.toml index 9522900..ab9b9e2 100644 --- a/simulation/config/BrightSide.toml +++ b/simulation/config/BrightSide.toml @@ -33,7 +33,7 @@ max_acceleration = 6 max_deceleration = 6 [motor_config] -motor_type = "AdvancedMotor" +motor_type = "BasicMotor" road_friction = 2.340e-02 tire_radius = 0.2032 vehicle_frontal_area = 1.1853 @@ -43,3 +43,10 @@ drag_coefficient = 1.166e-01 [motor_config.AdvancedMotor] cornering_coefficient = 1.0 + +[aeroshell_config] +drag_lookup = {0 = 0.137601477, 18 = 0.2335363083, 36 = 0.5965968882, 54= 1.224448936, 72= 1.861868971, 90= 2.38148208, 108= 2.073196244, 126= 1.587471653, 144= 0.5564901716, 162= 0.2141437734, 180= 0.1386601712 } +down_lookup = {0=0.37526598, 18= 0.3378390168, 36= 0.576439927, 54= 0.8675973423, 72= 1.19551954, 90= 2.683269654, 108= 2.223002744, 126= 1.581662338, 144= 0.17190782, 162= 0.1882638387, 180= 0.2153506426 } + + + diff --git a/simulation/config/models/_car.py b/simulation/config/models/_car.py index 576000a..8932fa2 100644 --- a/simulation/config/models/_car.py +++ b/simulation/config/models/_car.py @@ -102,6 +102,15 @@ class AdvancedMotorConfig(MotorConfig): cornering_coefficient: float +class AeroshellConfig(Config): + """ + Configuration object describing the aerodynamics forces (specifically drag and downforce) of a vehicle. + """ + model_config = ConfigDict(frozen=True) + drag_lookup: dict[float, float] #lookup table that corresponds angles to drag force, computed by a CFD + down_lookup: dict[float, float] #lookup table that corresponds angles to down force, computed by a CFD + + class RegenConfig(Config): """ Configuration object describing the regenerative braking systems of a vehicle. @@ -123,5 +132,6 @@ class CarConfig(Config): battery_config: BatteryConfig motor_config: MotorConfig regen_config: RegenConfig + aeroshell_config: AeroshellConfig name: str diff --git a/simulation/model/Model.py b/simulation/model/Model.py index da05c46..90b8c5f 100644 --- a/simulation/model/Model.py +++ b/simulation/model/Model.py @@ -15,6 +15,7 @@ from physics.models.battery import BaseBattery from physics.models.lvs import BaseLVS from physics.models.motor import BaseMotor +from physics.models.aeroshell import Aeroshell from physics.models.regen import BaseRegen from physics.environment.gis import BaseGIS from physics.environment.meteorology import BaseMeteorology @@ -36,6 +37,7 @@ def __init__( array: BaseArray, battery: BaseBattery, motor: BaseMotor, + aeroshell: Aeroshell, regen: BaseRegen, lvs: BaseLVS, gis: BaseGIS, @@ -56,6 +58,7 @@ def __init__( :param BaseArray array: An instance of `BaseArray` representing the solar array to be simulated. :param BaseBattery battery: An instance of `BaseBattery` representing the batter pack to be simulated. :param BaseMotor motor: An instance of `BaseMotor` representing the motor to be simulated. + :param Aeroshell aeroshell: An instance of the Aeroshell class that represents the drag and down force calculations :param BaseRegen regen: An instance of `BaseRegen` representing the regenerative braking system to be simulated. :param BaseLVS lvs: An instance of `BaseLVS` representing the low-voltage systems to be simulated. :param BaseGIS gis: An instance of `BaseGIS` which characterizes geographical information about the simulation. @@ -72,6 +75,7 @@ def __init__( self.simulation_dt = simulation_dt self.solar_array = array self.motor = motor + self.aeroshell = aeroshell self.regen = regen self.battery = battery self.gis = gis diff --git a/simulation/model/ModelBuilder.py b/simulation/model/ModelBuilder.py index a6df88e..b30fbff 100644 --- a/simulation/model/ModelBuilder.py +++ b/simulation/model/ModelBuilder.py @@ -28,6 +28,7 @@ from physics.models.battery import BaseBattery, BasicBattery, EquivalentCircuitBatteryModel, BatteryModelConfig from physics.models.lvs import BaseLVS, BasicLVS from physics.models.motor import BaseMotor, BasicMotor, AdvancedMotor +from physics.models.aeroshell import Aeroshell from physics.models.regen import BaseRegen, BasicRegen from physics.environment.gis import BaseGIS, GIS from physics.environment.meteorology import ( @@ -70,6 +71,7 @@ def __init__(self, cache: Cache = None): self.gis: Optional[BaseGIS] = None self.meteorology: Optional[BaseMeteorology] = None self.motor: Optional[BaseMotor] = None + self.aeroshell: Optional[Aeroshell] = None self.battery: Optional[BaseBattery] = None self.regen: Optional[BaseRegen] = None self.max_acceleration: Optional[float] = None @@ -424,6 +426,8 @@ def compile(self): **self._car_config.motor_config.model_dump() ) + self.aeroshell = Aeroshell(**self._car_config.aeroshell_config.model_dump()) + self.regen = BasicRegen(self._car_config.vehicle_config.vehicle_mass) self.num_laps = self.route_data.tiling @@ -474,6 +478,7 @@ def get(self) -> Model: array=self.array, battery=self.battery, motor=self.motor, + aeroshell=self.aeroshell, lvs=self.lvs, regen=self.regen, gis=self.gis, diff --git a/simulation/model/Simulation.py b/simulation/model/Simulation.py index 5e9ace8..2963d73 100644 --- a/simulation/model/Simulation.py +++ b/simulation/model/Simulation.py @@ -51,6 +51,8 @@ def __init__(self, model): self.tick_array = None self.time_zones = None self.distances = None + self.drag_force = None + self.down_force = None self.cumulative_distances = None self.closest_gis_indices = None self.closest_weather_indices = None @@ -73,6 +75,7 @@ def __init__(self, model): self.time_in_motion = None self.final_soc = None self.map_data_indices = None + self.wind_attack_angles = None def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) -> None: """ @@ -175,6 +178,12 @@ def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) self.wind_speeds = get_array_directional_wind_speed( self.gis_vehicle_bearings, self.absolute_wind_speeds, self.wind_directions ) + #get the wind attack angles + self.wind_attack_angles = self.model.meteorology.wind_direction - (self.gis_vehicle_bearings + 180) #convert azimuthal angle to meteorological convention + + # with calculated wind_speeds, we can now calculate (aerodynamic) drag and down forces in order to pass into motor model calculations + self.drag_force = self.model.aeroshell.calculate_drag(self.wind_speeds, self.wind_attack_angles, self.speed_kmh/3.6) + self.down_force = self.model.aeroshell.calculate_down(self.wind_speeds, self.wind_attack_angles, self.speed_kmh/3.6) # Get an array of solar irradiance at every coordinate and time self.solar_irradiances = self.model.meteorology.calculate_solar_irradiances( @@ -192,10 +201,8 @@ def run_simulation_calculations(self, speed_kmh: NDArray, track_speeds: NDArray) self.model.simulation_dt ) - coords = self.model.gis.get_path()[:self.model.gis.num_unique_coords] - coords_at_each_tick = coords[self.closest_gis_indices] self.motor_consumed_energy = self.model.motor.calculate_energy_in( - self.speed_kmh, self.gradients, self.wind_speeds, self.model.simulation_dt, coords_at_each_tick + self.speed_kmh, self.gradients, self.drag_force, self.down_force, self.model.simulation_dt ) self.array_produced_energy = self.model.solar_array.calculate_produced_energy( @@ -344,6 +351,9 @@ def get_results( "map_data_indices": self.map_data_indices, "path_coordinates": self.model.gis.get_path(), "raw_soc": self.raw_soc, + "drag_force": self.drag_force, + "down_force": self.down_force, + "wind_attack_angles": self.wind_attack_angles, } if "default" in requested_properties or requested_properties == "default": diff --git a/simulation/optimization/genetic.py b/simulation/optimization/genetic.py index fe12b69..5d88398 100644 --- a/simulation/optimization/genetic.py +++ b/simulation/optimization/genetic.py @@ -306,9 +306,6 @@ def __init__( # Bind the value of each gene to be between 0 and 1 as chromosomes should be normalized. gene_space = {"low": 0.0, "high": 1.0} - # Add a time delay between generations (used for debug purposes) - delay_after_generation = 0.0 - # Store diversity of generation per optimization iteration self.diversity = [] @@ -392,7 +389,7 @@ def get_initial_population( """ - population_file = population_directory / "initial_population.npz" + population_file = "initial_population.npz" arrays_from_cache = 0 new_initial_population = None diff --git a/uv.lock b/uv.lock index f62c33d..4c1d30e 100644 --- a/uv.lock +++ b/uv.lock @@ -1706,8 +1706,10 @@ dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/5e/4d/8694391d6b014bc49f8fd2eb8c05c94526b36fba8dc76d439f3d51948e46/timezonefinder-6.5.9.tar.gz", hash = "sha256:0d84c792a499fd098a35c701c3e3293423ba8d45c81b3eecd7c7cb72c7f1f415", size = 51435008 } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/aa/57fb0cb5b739a12ce6fc401b9d1e5d0b2ee092449c1d3ae14aa815409062/timezonefinder-6.5.9-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e9a0caf638f43b6dd9980731d1424500c7a6a5048db808aad7032560df5c663", size = 51443482 }, + { url = "https://files.pythonhosted.org/packages/70/de/c96b04236e9aa7902f4e416c5989fd019efb0e09e69f40cedb1e6e7bcd26/timezonefinder-6.5.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039919be603809b98f3d2b6a4ed33110ac7cc6bbd507959531401c91f3469fb", size = 51444624 }, { url = "https://files.pythonhosted.org/packages/2b/b9/c8a05e55096deea0b789d649da9729312ab1d97e3c52b0f0254c18f94cc7/timezonefinder-6.5.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c5347c4a73b40af4867a2946a5172ec68b644e7036888b9b5a0e568499bfc0f3", size = 51445521 }, { url = "https://files.pythonhosted.org/packages/53/6c/498b3f453b15d7ecf2ecf54a8f1cc7fbd1d3c3eed86a5182081ebc17b180/timezonefinder-6.5.9-cp313-cp313-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d1f30b550c24598459643285ef0f5469524ad7a32dd854199e7a2a463600ff", size = 51443403 }, + { url = "https://files.pythonhosted.org/packages/4e/5a/dfcd510203ccaf01099394850df7ee3733afffa57bfca3499b7f39484edf/timezonefinder-6.5.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb175d470e6f6abbeb13bd3a638bb1a0aa4124e1a93d040e5d933cf69076c60", size = 51444549 }, { url = "https://files.pythonhosted.org/packages/8b/16/6ae92a93dd703c0485e4c0a76e28bc829f9c7eba548a1b2e17fb3342e4c3/timezonefinder-6.5.9-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4f36877ad3a988f329cbd3f04f7cadc56dce073ad4a7bda7397f46ac2a61f9a4", size = 51445432 }, ] @@ -1842,7 +1844,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/6b/1c/614e7daf9845006ad [[package]] name = "ubc-solar-physics" -version = "1.8.2" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile" }, @@ -1881,18 +1883,18 @@ dependencies = [ { name = "urllib3" }, { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/6b/77e420561b851f545cfa1d0e270b5cfc1b3da00a6295990a1c3244255aed/ubc_solar_physics-1.8.2.tar.gz", hash = "sha256:3868f1033240ea8369e887411f0b0e3383178d8f6c4cd2dfb29c5ece67a263c2", size = 22460102 } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6d/ca790af6b0c9293c04a6bf78d6fb001f38568dd3240bbaa69e0587093d36/ubc_solar_physics-1.9.1.tar.gz", hash = "sha256:594d11cdb3e92092834a9288a3c353dcf653cc717edb04e30ac2930f1789359d", size = 22509102 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/98/f796f5531bbcfdcdf12ffe58139580781b7c0535d1795bd1bc9283027de1/ubc_solar_physics-1.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8f835438f9a5b21608413df8318a7a3789e690518d4adbc19f9488c6580a9bf5", size = 340184 }, - { url = "https://files.pythonhosted.org/packages/37/15/16acdc473ccf407a8c592d1bbfb36fa9a52114a088d979d6ee3afcc67009/ubc_solar_physics-1.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a99bef3278223f3aca844070a16c82e5e73b65ddf8882b165921e7321d4e7c6", size = 331885 }, - { url = "https://files.pythonhosted.org/packages/e7/d8/d251ec15b054ebf4a3fe142cafc1e81c532cf5cf121085667ea6bc9c0c08/ubc_solar_physics-1.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adc4e225939f0036f3729ee9b056deed64060bee8703261fd6375a8d0660fda", size = 367762 }, - { url = "https://files.pythonhosted.org/packages/2b/26/1c614bc80c25f9ddb9d8807edd041787d62e7a4465011ff57cc22292c8ff/ubc_solar_physics-1.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c7d820fc544e5e96febe440cc83ae50305fb4350848fc849a1af8274a77520", size = 383224 }, - { url = "https://files.pythonhosted.org/packages/ff/9a/bb58d5beec5078bfa7e1599f3f6bd0e0fabecd3117aabf7750499a3d8ea1/ubc_solar_physics-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:eb0bbf066b492c7b9340c8331c46d092b974c9c1a659c976493669d30902bbae", size = 225132 }, - { url = "https://files.pythonhosted.org/packages/0c/42/384f3f9154c1e8b84a7a5cceb62348fc83056672ba96a788dcdf60cb7976/ubc_solar_physics-1.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ceae39368db703dd46150d99a38f8ca5ec8172a311cf7046e6e587490551fb1", size = 340184 }, - { url = "https://files.pythonhosted.org/packages/f6/90/932e1c328a5cb2ee2e5748ae16759ac523ae886e342b1cb20632c9b0631a/ubc_solar_physics-1.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72e96c11179013687e98353d82badf26f2a01eceb6b427f290b56fd32098ba07", size = 331887 }, - { url = "https://files.pythonhosted.org/packages/d1/fc/e4966d56fa650372b48b1c54cb4e0905a875a9c5d6afecf925863d392d35/ubc_solar_physics-1.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f08a3072bcc90b1f14f9b4f4a190bb18f2e5b63961e6774c8a135543d313132", size = 367763 }, - { url = "https://files.pythonhosted.org/packages/be/fb/e924aa61b035a567ce5f006b8a0e9a52c8851479c2234b30358890fe1541/ubc_solar_physics-1.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e6791e99aae088a7bdba52241d572daa3ca9a124a4f1fc724c04258e283d839", size = 383226 }, - { url = "https://files.pythonhosted.org/packages/92/68/593bac4c33cf2162d2977dad98bfbcaa47959bed8c696870f37bf0e7598e/ubc_solar_physics-1.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb1706181c9621611c8649f2ba3aab8510b538c5a6e0d8190ef3e82b3210916c", size = 225134 }, + { url = "https://files.pythonhosted.org/packages/48/0b/a603789d8bb98e94b9775d9dc3061605d8fa48f3260d748b135e9ae47bd5/ubc_solar_physics-1.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2c80e0067dc7e4cdc68ed1c1fdbbe7fd39c12358c93d7d4667f67b76cc4eeac3", size = 387180 }, + { url = "https://files.pythonhosted.org/packages/0e/05/dbfbfe49120a232c41511b77fb6af02b65179af26f3d69043b24a9737ff4/ubc_solar_physics-1.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:897bf1df66b50e632d2cc1569a24faa7849b1ce55b2e44e9b21c9bfc58c8c0a9", size = 377756 }, + { url = "https://files.pythonhosted.org/packages/8f/08/afa4058cab73cfab01b465968b1d79c3b36df1a6da9880540249d17bd034/ubc_solar_physics-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fa00609f2271732fb6f80f6309aa55a0eeb6f97625bd7a62a63eaf706171d8", size = 414809 }, + { url = "https://files.pythonhosted.org/packages/46/90/855c7e28ddf64346d69ea98ebb34d4c4e126b7bbfebbf2f4464debd34a6b/ubc_solar_physics-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97ee3edbb3fec08e1edab764ca39c3718cf77457a47812f809c38433c5823fec", size = 429296 }, + { url = "https://files.pythonhosted.org/packages/82/d1/ba0de3f95a6d1bddce0eedccb6600aa8a73a206c628d48ca8994d42f4c43/ubc_solar_physics-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:16177e72589965a062bcdc57131bfa32def173377a962edbddaa5ffcfaa05a68", size = 270643 }, + { url = "https://files.pythonhosted.org/packages/c9/9b/f01dd2d00964266ecd1db89cca2131035fc4655051e35f4f5a58afc529ce/ubc_solar_physics-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2f16994df9b6c937f29b1a70a0c609af9b1f1f93ef383dfd1ed5da88ee9e6cd", size = 387180 }, + { url = "https://files.pythonhosted.org/packages/a2/6e/170c96d2bc2a279b590f82a9308175f188fb791008761d54301013e9fcb8/ubc_solar_physics-1.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:124707ea07e91aff91ccc20bd910a208140cdd00aee675cfe1b6c9e2efca3ba4", size = 377755 }, + { url = "https://files.pythonhosted.org/packages/3c/39/2797b662d0dac4e9d5cbd876d57054f8246b74398ed6cc1ddf85bc06f967/ubc_solar_physics-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2081c3cfdfed6ae98f1efef862f20dd2b89f18b0531f274f9d4fe65ad297f912", size = 414810 }, + { url = "https://files.pythonhosted.org/packages/90/b5/e46d0fd09e7b26924bdd1b39e26b0663a13f35d9fbcd4e6a239df32c29cb/ubc_solar_physics-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a09e418c09c4371e65e3d46fca239bbde613559245d77527d9745cf9bee55d21", size = 429297 }, + { url = "https://files.pythonhosted.org/packages/83/a6/b761f242388209e4f4825065738a12db8166806a8b74ca1addf7656b229e/ubc_solar_physics-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ce7a018abd8ac4c72a365cd347907a2c04c1ca3074c4b31ed900bd9e5beec85", size = 270647 }, ] [[package]] @@ -1991,7 +1993,7 @@ requires-dist = [ { name = "tqdm", specifier = ">=4.64.0" }, { name = "tzlocal", specifier = ">=5.2" }, { name = "ubc-solar-data-tools", specifier = "==1.9.1" }, - { name = "ubc-solar-physics", specifier = "==1.8.2" }, + { name = "ubc-solar-physics", specifier = "==1.9.1" }, ] provides-extras = ["dev"]