ubc-solar-physics 1.4.2__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. ubc_solar_physics-1.6.0/.gitattributes +1 -0
  2. ubc_solar_physics-1.6.0/.github/PULL_REQUEST_TEMPLATE.md +24 -0
  3. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/.github/workflows/build_and_publish.yaml +4 -4
  4. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/PKG-INFO +2 -2
  5. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/docs_requirements.txt +2 -2
  6. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/conf.py +7 -14
  7. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/_version.py +4 -9
  8. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/gis/base_gis.py +14 -0
  9. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/gis/gis.py +34 -0
  10. ubc_solar_physics-1.6.0/physics/environment/gis/gis.rs +113 -0
  11. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/lib.rs +29 -5
  12. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/motor/__init__.py +3 -1
  13. ubc_solar_physics-1.6.0/physics/models/motor/advanced_motor.py +196 -0
  14. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/motor/base_motor.py +2 -0
  15. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/motor/basic_motor.py +33 -14
  16. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/pyproject.toml +3 -3
  17. ubc_solar_physics-1.6.0/tests/gis_tests/test_calculate_driving_speeds.py +70 -0
  18. ubc_solar_physics-1.6.0/tests/gis_tests/test_gis_driving_speeds.py +322 -0
  19. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/ubc_solar_physics.egg-info/PKG-INFO +2 -2
  20. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/ubc_solar_physics.egg-info/SOURCES.txt +5 -1
  21. ubc_solar_physics-1.4.2/.github/pull_request_template.md +0 -1
  22. ubc_solar_physics-1.4.2/physics/environment/gis/gis.rs +0 -25
  23. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/.github/workflows/run_tests.yaml +0 -0
  24. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/.gitignore +0 -0
  25. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/.readthedocs.yaml +0 -0
  26. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/Cargo.lock +0 -0
  27. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/Cargo.toml +0 -0
  28. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/LICENSE +0 -0
  29. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/README.md +0 -0
  30. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/data.png +0 -0
  31. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/Makefile +0 -0
  32. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/_generate_version.py +0 -0
  33. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/make.bat +0 -0
  34. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/README.md +0 -0
  35. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/api.rst +0 -0
  36. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/environment/gis.rst +0 -0
  37. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/environment/index.rst +0 -0
  38. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/environment/meteorology.rst +0 -0
  39. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/index.rst +0 -0
  40. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/arrays.rst +0 -0
  41. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/battery.rst +0 -0
  42. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/index.rst +0 -0
  43. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/lvs.rst +0 -0
  44. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/motor.rst +0 -0
  45. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/docs/source/models/regen.rst +0 -0
  46. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/battery_model_examples/battery_config.toml +0 -0
  47. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/battery_model_examples/battery_model_example.py +0 -0
  48. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/battery_model_examples/data_py.png +0 -0
  49. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/battery_model_examples/data_rust.png +0 -0
  50. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/data_query/current.csv +0 -0
  51. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/data_query/poetry.lock +0 -0
  52. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/data_query/pyproject.toml +0 -0
  53. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/data_query/query_data.py +0 -0
  54. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/data_query/voltage.csv +0 -0
  55. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/kalman_filter_examples/battery_config.toml +0 -0
  56. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/kalman_filter_examples/current.csv +0 -0
  57. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/kalman_filter_examples/kalman_filter_example.py +0 -0
  58. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/examples/kalman_filter_examples/voltage.csv +0 -0
  59. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/__init__.py +0 -0
  60. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/__init__.py +0 -0
  61. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/environment.rs +0 -0
  62. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/gis/__init__.py +0 -0
  63. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/gis.rs +0 -0
  64. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology/__init__.py +0 -0
  65. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology/base_meteorology.py +0 -0
  66. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology/clouded_meteorology.py +0 -0
  67. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology/irradiant_meteorology.py +0 -0
  68. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology/meteorology.rs +0 -0
  69. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment/meteorology.rs +0 -0
  70. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/environment.rs +0 -0
  71. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/__init__.py +0 -0
  72. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/arrays/__init__.py +0 -0
  73. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/arrays/arrays.rs +0 -0
  74. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/arrays/base_array.py +0 -0
  75. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/arrays/basic_array.py +0 -0
  76. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/arrays.rs +0 -0
  77. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/__init__.py +0 -0
  78. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/base_battery.py +0 -0
  79. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/basic_battery.py +0 -0
  80. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/battery.rs +0 -0
  81. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/battery_config.py +0 -0
  82. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/battery_config.toml +0 -0
  83. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/battery_model.py +0 -0
  84. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery/kalman_filter.py +0 -0
  85. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/battery.rs +0 -0
  86. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/constants.py +0 -0
  87. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/lvs/__init__.py +0 -0
  88. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/lvs/base_lvs.py +0 -0
  89. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/lvs/basic_lvs.py +0 -0
  90. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/lvs/lvs.rs +0 -0
  91. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/lvs.rs +0 -0
  92. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/motor/motor.rs +0 -0
  93. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/motor.rs +0 -0
  94. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/regen/__init__.py +0 -0
  95. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/regen/base_regen.py +0 -0
  96. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/regen/basic_regen.py +0 -0
  97. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/regen/regen.rs +0 -0
  98. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models/regen.rs +0 -0
  99. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/physics/models.rs +0 -0
  100. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/setup.cfg +0 -0
  101. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/battery_config.toml +0 -0
  102. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/kalman_filter_tests/current.csv +0 -0
  103. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/kalman_filter_tests/test_basic_kalman_filter.py +0 -0
  104. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/kalman_filter_tests/test_filter_real_data.py +0 -0
  105. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/kalman_filter_tests/voltage.csv +0 -0
  106. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/tests/test_versioning.py +0 -0
  107. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/ubc_solar_physics.egg-info/dependency_links.txt +0 -0
  108. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/ubc_solar_physics.egg-info/requires.txt +0 -0
  109. {ubc_solar_physics-1.4.2 → ubc_solar_physics-1.6.0}/ubc_solar_physics.egg-info/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ **/*.csv linguist-generated=true
@@ -0,0 +1,24 @@
1
+ # [FEAT/FIX/DOCS]: Pull Request Name
2
+
3
+ - [ ] Linter Check Succeeded
4
+ - [ ] All Tests Succeeded
5
+ - [ ] Module Documentation
6
+ - [ ] Method/Class/Function Documentation
7
+ - [ ] Documentation Correctly Builds
8
+ - [x] Pull Request Completed
9
+
10
+ ## What's New
11
+ [What has been added?]
12
+
13
+ ## Bugfixes
14
+ [Have you added any bug fixes?]
15
+
16
+ ## Deprecated/Removed
17
+ [Has anything been removed or deprecated?]
18
+
19
+ ## Dependencies
20
+ [Any changes in dependencies?]
21
+
22
+ ## Notes
23
+ [Anything else relevant?]
24
+
@@ -20,12 +20,12 @@ jobs:
20
20
 
21
21
  steps:
22
22
  - name: Checkout
23
- uses: actions/checkout@v2
23
+ uses: actions/checkout@v4
24
24
  with:
25
25
  fetch-depth: 0 # Required to fetch full history including tags, so setuptools_scm can determine version
26
26
 
27
27
  - name: Setup Python
28
- uses: actions/setup-python@v1
28
+ uses: actions/setup-python@v4
29
29
  with:
30
30
  python-version: '3.9'
31
31
 
@@ -59,7 +59,7 @@ jobs:
59
59
  fetch-depth: 0 # Required to fetch full history including tags, so setuptools_scm can determine version
60
60
 
61
61
  - name: Set up Python
62
- uses: actions/setup-python@v2
62
+ uses: actions/setup-python@v4
63
63
 
64
64
  # We run `python -m setuptools_scm` and `git status` for debugging purposes, to verify that
65
65
  # setuptools_scm is properly identifying the version based on the tag
@@ -92,7 +92,7 @@ jobs:
92
92
  run: cibuildwheel --output-dir dist
93
93
  env:
94
94
  CIBW_ARCHS_MACOS: "x86_64 arm64" # Make sure both x86_64 and arm64 wheels are built for macOS
95
- CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH"'
95
+ CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH" MACOSX_DEPLOYMENT_TARGET="10.12"'
96
96
  CIBW_ENVIRONMENT_WINDOWS: 'PATH="$UserProfile\.cargo\bin;$PATH"'
97
97
  CIBW_BEFORE_BUILD: >
98
98
  pip install -U setuptools-rust &&
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.1
2
2
  Name: ubc-solar-physics
3
- Version: 1.4.2
3
+ Version: 1.6.0
4
4
  Summary: UBC Solar's Simulation Environment
5
5
  Author: Fisher Xue, Mihir Nimgade, Chris Chang, David Widjaja, Justin Hua, Ilya Veksler, Renu Rajamagesh, Ritchie Xia, Erik Langille, Chris Aung, Nicolas Ric, Ishaan Trivedi, Jason Liang, Felix Toft, Mack Wilson, Jonah Lee, Tamzeed Quazi, Joshua Riefman
6
6
  Author-email: UBC Solar <strategy@ubcsolar.com>
@@ -38,7 +38,6 @@ rich
38
38
  snowballstemmer
39
39
  Sphinx
40
40
  sphinx-autodoc-typehints
41
- sphinx-rtd-theme==2.0.0
42
41
  sphinxcontrib-applehelp
43
42
  sphinxcontrib-devhelp
44
43
  sphinxcontrib-htmlhelp
@@ -50,4 +49,5 @@ tqdm
50
49
  urllib3
51
50
  zipp
52
51
  setuptools_scm
53
- pydata-sphinx-theme==0.15.4
52
+ pydata-sphinx-theme==0.15.4
53
+ ubc-solar-physics==1.5.0
@@ -1,16 +1,9 @@
1
- # from setuptools_scm import get_version
2
- # from pathlib import Path
3
- # import importlib
4
-
5
- # Generate the version file
6
- # version = get_version()
7
- # version_file = Path('physics/_version.py')
8
- # version_file.write_text(f"__version__ = '{version}'\n")
9
-
10
- # Dynamically import the version
11
- # importlib.invalidate_caches()
12
- # physics_module = importlib.import_module("physics")
13
- # __version__ = physics_module.__version__
1
+ import os
2
+ import sys
3
+ sys.path.insert(0, os.path.abspath('../../')) # Adjust as necessary
4
+ print(sys.path)
5
+ print(os.listdir(os.getcwd()))
6
+ print(os.listdir(sys.path[0]))
14
7
 
15
8
  # Configuration file for the Sphinx documentation builder.
16
9
  #
@@ -23,7 +16,7 @@
23
16
  project = 'UBC Solar Physics'
24
17
  copyright = '2024, UBC Solar'
25
18
  author = 'Joshua Riefman'
26
- release = '1.0.0'
19
+ release = "1.0.0"
27
20
 
28
21
  # -- General configuration ---------------------------------------------------
29
22
  # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -1,13 +1,8 @@
1
- # file generated by setuptools-scm
1
+ # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
-
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
-
6
3
  TYPE_CHECKING = False
7
4
  if TYPE_CHECKING:
8
- from typing import Tuple
9
- from typing import Union
10
-
5
+ from typing import Tuple, Union
11
6
  VERSION_TUPLE = Tuple[Union[int, str], ...]
12
7
  else:
13
8
  VERSION_TUPLE = object
@@ -17,5 +12,5 @@ __version__: str
17
12
  __version_tuple__: VERSION_TUPLE
18
13
  version_tuple: VERSION_TUPLE
19
14
 
20
- __version__ = version = '1.4.2'
21
- __version_tuple__ = version_tuple = (1, 4, 2)
15
+ __version__ = version = '1.6.0'
16
+ __version_tuple__ = version_tuple = (1, 6, 0)
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import numpy as np
3
+ from numpy.typing import ArrayLike, NDArray
3
4
 
4
5
 
5
6
  class BaseGIS(ABC):
@@ -22,3 +23,16 @@ class BaseGIS(ABC):
22
23
  @abstractmethod
23
24
  def get_path(self) -> np.ndarray:
24
25
  raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def calculate_current_heading_array(self) -> np.ndarray:
29
+ raise NotImplementedError
30
+
31
+ def calculate_driving_speeds(
32
+ self,
33
+ average_lap_speeds: ArrayLike,
34
+ simulation_dt: int,
35
+ driving_allowed: ArrayLike,
36
+ idle_time: int
37
+ ) -> NDArray[float]:
38
+ raise NotImplementedError
@@ -4,6 +4,7 @@ import core
4
4
  import numpy as np
5
5
  import sys
6
6
 
7
+ from numpy.typing import ArrayLike, NDArray
7
8
  from tqdm import tqdm
8
9
  from xml.dom import minidom
9
10
  from haversine import haversine, Unit
@@ -82,6 +83,39 @@ class GIS(BaseGIS):
82
83
  """
83
84
  return core.closest_gis_indices_loop(distances, self.path_distances)
84
85
 
86
+ def calculate_driving_speeds(
87
+ self,
88
+ average_lap_speeds: ArrayLike,
89
+ simulation_dt: int,
90
+ driving_allowed: ArrayLike,
91
+ idle_time: int
92
+ ) -> NDArray[float]:
93
+ """
94
+ Generate valid driving speeds as a simulation-time array given a set of average speeds for each
95
+ simulated lap.
96
+ Driving speeds will only be non-zero when we are allowed to drive, and the speed
97
+ for every tick during a lap will be that lap's corresponding desired average speed for as long
98
+ as it takes to complete the lap.
99
+
100
+ :param average_lap_speeds: An array of average speeds in m/s, one for each simulated lap.
101
+ If there are more speeds given than laps available, the unused speeds will be silently ignored.
102
+ If there are too few, an error will be returned.
103
+ :param simulation_dt: The simulated tick length.
104
+ :param driving_allowed: A simulation-time boolean where the `True` elements are when we
105
+ are allowed to drive, and `False` is when we are not. Requires that (at least) the first element is
106
+ `False` due to the race beginning in the morning before we are allowed to drive.
107
+ :param idle_time: The length of time to pause driving upon processing a "0m/s" average speed.
108
+ :return: A simulation-time array of driving speeds in m/s, or an error if there weren't enough
109
+ laps provided to fill the entire simulation time.
110
+ """
111
+ return core.get_driving_speeds(
112
+ np.array(average_lap_speeds).astype(np.float64),
113
+ simulation_dt,
114
+ np.array(driving_allowed).astype(bool),
115
+ self.path_length,
116
+ idle_time
117
+ )
118
+
85
119
  @staticmethod
86
120
  def _python_calculate_closest_gis_indices(distances, path_distances):
87
121
  """
@@ -0,0 +1,113 @@
1
+ use numpy::ndarray::{ArrayViewD, ArrayView1};
2
+
3
+ pub fn rust_closest_gis_indices_loop(
4
+ distances: ArrayViewD<'_, f64>,
5
+ path_distances: ArrayViewD<'_, f64>,
6
+ ) -> Vec<i64> {
7
+ let mut current_coord_index: usize = 0;
8
+ let mut distance_travelled: f64 = 0.0;
9
+ let mut result: Vec<i64> = Vec::with_capacity(distances.len());
10
+
11
+ for &distance in distances {
12
+ distance_travelled += distance;
13
+
14
+ while distance_travelled > path_distances[current_coord_index] {
15
+ distance_travelled -= path_distances[current_coord_index];
16
+ current_coord_index += 1;
17
+ if current_coord_index >= path_distances.len() {
18
+ current_coord_index = 0;
19
+ }
20
+ }
21
+
22
+ current_coord_index = std::cmp::min(current_coord_index, path_distances.len() - 1);
23
+ result.push(current_coord_index as i64);
24
+ }
25
+
26
+ result
27
+ }
28
+
29
+ ///
30
+ /// Generate valid driving speeds as a simulation-time array given a set of average speeds for each
31
+ /// simulated lap.
32
+ /// Driving speeds will only be non-zero when we are allowed to drive, and the speed
33
+ /// for every tick during a lap will be that lap's corresponding desired average speed for as long
34
+ /// as it takes to complete the lap.
35
+ /// An average speed of 0m/s for a lap will be interpreted as "sit and charge" for `idle_time`
36
+ /// ticks.
37
+ ///
38
+ /// # Arguments
39
+ ///
40
+ /// * `average_speeds`: An array of average speeds in m/s, one for each simulated lap. If there are more
41
+ /// speeds given than laps available, the unused speeds will be silently ignored. If there are too
42
+ /// few, an error will be returned.
43
+ /// * `simulation_dt`: The simulated tick length
44
+ /// * `driving_allowed_boolean`: A simulation-time boolean where the `True` elements are when we
45
+ /// are allowed to drive, and `False` is when we are not.
46
+ /// * `track_length`: The length of the track in meters.
47
+ /// * `idle_time`: The number of ticks to "sit and charge" when desired.
48
+ ///
49
+ /// Returns: A simulation-time array of driving speeds in m/s, or an error if there weren't enough
50
+ /// laps provided to fill the entire simulation time.
51
+ ///
52
+ pub fn get_driving_speeds(
53
+ average_speeds: ArrayView1<'_, f64>, // Average speeds in m/s
54
+ simulation_dt: i64, // Time step in seconds
55
+ driving_allowed_boolean: ArrayView1<'_, bool>, // Simulation-time boolean array
56
+ track_length: f64, // Track length in meters
57
+ idle_time: i64 // Time to idle in seconds
58
+ ) -> Result<Vec<f64>, &'static str> {
59
+ let ticks_to_complete_lap: Vec<i64> = average_speeds.iter().map(| &average_speed | {
60
+ if average_speed > 0.0 {
61
+ // The number of ticks is the number of seconds, divided by seconds per tick
62
+ (track_length / average_speed / simulation_dt as f64).ceil() as i64
63
+ } else {
64
+ (idle_time as f64 / simulation_dt as f64).ceil() as i64
65
+ }
66
+ }).collect();
67
+
68
+ let mut lap_index: usize = 0;
69
+ let mut lap_speed: f64 = average_speeds[lap_index];
70
+
71
+ let mut ticks_to_lap_completion: i64 = ticks_to_complete_lap[lap_index];
72
+
73
+ let mut driving_speeds: Vec<f64> = Vec::with_capacity(driving_allowed_boolean.len());
74
+ for driving_allowed in driving_allowed_boolean.iter() {
75
+ if !driving_allowed {
76
+ // If we aren't allowed to drive, speed should be zero. Also, we should mark that we are
77
+ // done our lap since it means we ended the day in the middle of the lap, and we will
78
+ // start the next day at the beginning of a new lap, not where we ended off.
79
+
80
+ // If it's the first lap, we don't want to skip because we are probably in the morning
81
+ // where we haven't begun driving yet.
82
+ if lap_index > 0 {
83
+ ticks_to_lap_completion = 0;
84
+ }
85
+
86
+ driving_speeds.push(0.0)
87
+ } else {
88
+ // If we are driving, we should decrement ticks to lap completion. If its already
89
+ // zero, that means that we are done the lap and should move onto the next lap.
90
+ if ticks_to_lap_completion > 0 {
91
+ ticks_to_lap_completion -= 1;
92
+
93
+ driving_speeds.push(lap_speed)
94
+ } else {
95
+ // To advance to the next lap, increment the index and evaluate new variables
96
+ lap_index += 1;
97
+ if lap_index >= average_speeds.len() {
98
+ return Err("Not enough average speeds!")
99
+ }
100
+
101
+ // We subtract 1 since this iteration counts for the next lap, not the one
102
+ // that we just finished
103
+ ticks_to_lap_completion = ticks_to_complete_lap[lap_index] - 1;
104
+ lap_speed = average_speeds[lap_index];
105
+
106
+ driving_speeds.push(lap_speed)
107
+ }
108
+ }
109
+
110
+ }
111
+
112
+ Ok(driving_speeds)
113
+ }
@@ -1,13 +1,12 @@
1
- use chrono::{Datelike, NaiveDateTime, Timelike};
2
- use numpy::ndarray::{s, Array, Array2, ArrayViewD, ArrayViewMut2, ArrayViewMut3, Axis};
3
- use numpy::{PyArray, PyArrayDyn, PyReadwriteArrayDyn};
1
+ use numpy::ndarray::ArrayViewD;
2
+ use numpy::{PyArray, PyArrayDyn, PyReadwriteArrayDyn, PyReadonlyArray1, PyArray1};
4
3
  use pyo3::prelude::*;
5
4
  use pyo3::types::PyModule;
6
5
 
7
6
  pub mod environment;
8
7
  pub mod models;
9
- use crate::environment::gis::gis::rust_closest_gis_indices_loop;
10
- use crate::environment::meteorology::meteorology::{rust_calculate_array_ghi_times, rust_closest_weather_indices_loop, rust_weather_in_time, rust_closest_timestamp_indices};
8
+ use crate::environment::gis::gis::{rust_closest_gis_indices_loop, get_driving_speeds};
9
+ use crate::environment::meteorology::meteorology::{rust_calculate_array_ghi_times, rust_closest_weather_indices_loop, rust_weather_in_time};
11
10
  use crate::models::battery::battery::update_battery_array;
12
11
 
13
12
  fn constrain_speeds(speed_limits: ArrayViewD<f64>, speeds: ArrayViewD<f64>, tick: i32) -> Vec<f64> {
@@ -128,5 +127,30 @@ fn rust_simulation(_py: Python, m: &PyModule) -> PyResult<()> {
128
127
  (py_soc_array, py_voltage_array)
129
128
  }
130
129
 
130
+ #[pyfn(m)]
131
+ #[pyo3(name = "get_driving_speeds")]
132
+ fn py_get_driving_speeds<'py>(
133
+ py: Python<'py>,
134
+ py_average_speeds: PyReadonlyArray1<'py, f64>, // Average speeds in m/s
135
+ simulation_dt: i64, // Time step in seconds
136
+ py_driving_allowed_boolean: PyReadonlyArray1<'py, bool>, // Simulation-time boolean array
137
+ track_length: f64, // Track length in meters
138
+ idle_time: i64 // Time to idle in seconds
139
+ ) -> PyResult<&'py PyArray1<f64>> {
140
+ let average_speeds = py_average_speeds.as_array();
141
+ let driving_allowed_boolean = py_driving_allowed_boolean.as_array();
142
+
143
+ match get_driving_speeds(
144
+ average_speeds,
145
+ simulation_dt,
146
+ driving_allowed_boolean,
147
+ track_length,
148
+ idle_time
149
+ ) {
150
+ Ok(driving_speeds) => Ok(PyArray1::from_vec(py, driving_speeds)),
151
+ Err(error) => Err(pyo3::exceptions::PyValueError::new_err(error))
152
+ }
153
+ }
154
+
131
155
  Ok(())
132
156
  }
@@ -1,7 +1,9 @@
1
1
  from .base_motor import BaseMotor
2
2
  from .basic_motor import BasicMotor
3
+ from .advanced_motor import AdvancedMotor
3
4
 
4
5
  __all__ = [
5
6
  "BaseMotor",
6
- "BasicMotor"
7
+ "BasicMotor",
8
+ "AdvancedMotor",
7
9
  ]
@@ -0,0 +1,196 @@
1
+ import numpy as np
2
+ from haversine import haversine, Unit
3
+ from numpy.typing import NDArray
4
+ from physics.models.motor import BasicMotor
5
+
6
+
7
+ class AdvancedMotor(BasicMotor):
8
+ def __init__(self, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.cornering_coefficient = 15 # tuned to Day 1 and 3 FSGP data
11
+
12
+ def calculate_energy_in(self, required_speed_kmh, gradients, wind_speeds, tick, coords):
13
+ """
14
+ A function which takes in array of elevation, array of wind speed, required
15
+ speed, returns the consumed energy.
16
+
17
+ :param np.ndarray required_speed_kmh: (float[N]) required speed array in km/h
18
+ :param np.ndarray gradients: (float[N]) gradient at parts of the road
19
+ :param np.ndarray wind_speeds: (float[N]) speeds of wind in m/s, where > 0 means against the direction of the vehicle
20
+ :param float tick: length of 1 update cycle in seconds
21
+ :param np.ndarray coords: ([float[N,2]) The lat,lon coordinate of the car at each tick
22
+ :returns: (float[N]) energy expended by the motor at every tick
23
+ :rtype: np.ndarray
24
+
25
+ """
26
+ net_force, required_angular_speed_rads = self.calculate_net_force(required_speed_kmh, wind_speeds, gradients)
27
+
28
+ cornering_work = self.calculate_cornering_losses(required_speed_kmh, coords, tick)
29
+
30
+ motor_output_energies = required_angular_speed_rads * net_force * self.tire_radius * tick + cornering_work
31
+ motor_output_energies = np.clip(motor_output_energies, a_min=0, a_max=None)
32
+
33
+ e_m = self.calculate_motor_efficiency(required_angular_speed_rads, motor_output_energies, tick)
34
+ e_mc = self.calculate_motor_controller_efficiency(required_angular_speed_rads, motor_output_energies, tick)
35
+
36
+ motor_controller_input_energies = motor_output_energies / (e_m * e_mc)
37
+
38
+ # Filter out and replace negative energy consumption as 0
39
+ motor_controller_input_energies = np.where(motor_controller_input_energies > 0,
40
+ motor_controller_input_energies, 0)
41
+
42
+ return motor_controller_input_energies
43
+
44
+ def calculate_cornering_losses(self, required_speed_kmh, coords, tick):
45
+ """
46
+ Calculate the energy losses due to cornering based on vehicle speed and trajectory.
47
+
48
+ :param np.ndarray required_speed_kmh: (float[N]) Required speed array in km/h
49
+ :param np.ndarray coords: (float[N, 2]) Array containing latitude and longitude coordinates of the car at each tick
50
+ :param float tick: Length of one update cycle in seconds
51
+ :returns: (float[N]) Energy loss due to cornering at each tick
52
+ :rtype: np.ndarray
53
+ """
54
+ required_speed_ms = required_speed_kmh / 3.6
55
+ cornering_radii = self.calculate_radii(coords)
56
+
57
+ centripetal_lateral_force = self.vehicle_mass * (required_speed_ms ** 2) / cornering_radii
58
+ centripetal_lateral_force = np.clip(centripetal_lateral_force, a_min=0, a_max=10000)
59
+
60
+ slip_angles_degrees = self.get_slip_angle_for_tire_force(centripetal_lateral_force)
61
+ slip_angles_radians = np.radians(slip_angles_degrees)
62
+ slip_distances = np.tan(slip_angles_radians) * required_speed_ms * tick
63
+
64
+ return slip_distances * centripetal_lateral_force * self.cornering_coefficient
65
+
66
+ def calculate_radii(self, coords):
67
+ """
68
+ Calculate the cornering radii for a given set of waypoints.
69
+
70
+ :param np.ndarray coords: (float[N, 2]) Array containing latitude and longitude coordinates of the car's path
71
+ :returns: (float[N]) Array of cornering radii at each waypoint
72
+ :rtype: np.ndarray
73
+ """
74
+
75
+ # pop off last coordinate if first and last coordinate are the same
76
+ repeated_last_coordinate = False
77
+ if np.array_equal(coords[0], coords[len(coords) - 1]):
78
+ coords = coords[:-1]
79
+ repeated_last_coordinate = True
80
+
81
+ cornering_radii = np.empty(len(coords))
82
+ for i in range(len(coords)):
83
+ # if the next point or previous point is out of bounds, wrap the index around the array
84
+ i2 = (i - 1) % len(coords)
85
+ i3 = (i + 1) % len(coords)
86
+ current_point = coords[i]
87
+ previous_point = coords[i2]
88
+ next_point = coords[i3]
89
+
90
+ x1 = 0
91
+ y1 = 0
92
+ x2, y2 = self.calculate_meter_distance(current_point, previous_point)
93
+ x3, y3 = self.calculate_meter_distance(current_point, next_point)
94
+ cornering_radii[i] = self.radius_of_curvature(x1, y1, x2, y2, x3, y3)
95
+
96
+ # If the last coordinate was removed, duplicate the first radius value to the end of the array
97
+ if repeated_last_coordinate:
98
+ cornering_radii = np.append(cornering_radii, cornering_radii[0])
99
+
100
+ # ensure that super large radii are bounded by a large number, like 10000
101
+ cornering_radii = np.where(np.isnan(cornering_radii), 10000, cornering_radii)
102
+ cornering_radii = np.where(cornering_radii > 10000, 10000, cornering_radii)
103
+
104
+ return cornering_radii
105
+
106
+ def generate_slip_angle_lookup(self, min_degrees, max_degrees, num_elements):
107
+ """
108
+ Generate a lookup table of slip angles and corresponding tire forces using Pacejka's Magic Formula.
109
+
110
+ https://www.edy.es/dev/docs/pacejka-94-parameters-explained-a-comprehensive-guide/
111
+
112
+ :param float min_degrees: Minimum slip angle in degrees
113
+ :param float max_degrees: Maximum slip angle in degrees
114
+ :param int num_elements: Number of discrete elements in the lookup table
115
+ :returns: (float[num_elements], float[num_elements]) Arrays of slip angles (degrees) and corresponding tire forces (Newtons)
116
+ :rtype: tuple[np.ndarray, np.ndarray]
117
+ """
118
+
119
+ b = .25 # Stiffness
120
+ c = 2.2 # Shape
121
+ d = 2.75 # Peak
122
+ e = 1.0 # Curvature
123
+
124
+ fz = self.vehicle_mass * 9.81 # Newtons
125
+
126
+ slip_angles = np.linspace(min_degrees, max_degrees, num_elements)
127
+ tire_forces = fz * d * np.sin(
128
+ c * np.arctan(b * slip_angles - e * (b * slip_angles - np.arctan(b * slip_angles))))
129
+
130
+ return slip_angles, tire_forces
131
+
132
+ def get_slip_angle_for_tire_force(self, desired_tire_force):
133
+ slip_angles, tire_forces = self.generate_slip_angle_lookup(0, 70, 100000)
134
+
135
+ # Use the numpy interpolation function to find slip angle for the given tire force
136
+ estimated_slip_angle = np.interp(desired_tire_force, tire_forces, slip_angles)
137
+
138
+ return estimated_slip_angle
139
+
140
+ @staticmethod
141
+ def calculate_meter_distance(coord1: NDArray, coord2: NDArray):
142
+ """
143
+ Calculate the x and y distance in meters between two latitude-longitude coordinates.
144
+
145
+ :param tuple coord1: (float[2]) The (latitude, longitude) coordinates of the first point
146
+ :param tuple coord2: (float[2]) The (latitude, longitude) coordinates of the second point
147
+ :returns: (float[2]) The x (longitude) and y (latitude) distances in meters
148
+ :rtype: tuple
149
+ """
150
+ lat1, lon1 = coord1
151
+ lat2, lon2 = coord2
152
+
153
+ # Base coordinate
154
+ coord_base = (lat1, lon1)
155
+ # Coordinate for latitude difference (keep longitude the same)
156
+ coord_lat = (lat2, lon1)
157
+ # Coordinate for longitude difference (keep latitude the same)
158
+ coord_long = (lat1, lon2)
159
+
160
+ # Calculate y distance (latitude difference)
161
+ y_distance = haversine(coord_base, coord_lat, unit=Unit.METERS)
162
+ # Calculate x distance (longitude difference)
163
+ x_distance = haversine(coord_base, coord_long, unit=Unit.METERS)
164
+
165
+ if lat2 < lat1:
166
+ y_distance = -y_distance
167
+ if lon2 < lon1:
168
+ x_distance = -x_distance
169
+
170
+ return x_distance, y_distance
171
+
172
+ @staticmethod
173
+ def radius_of_curvature(x1, y1, x2, y2, x3, y3):
174
+ """
175
+ Uses the circumcircle the radius of curvature of a circle passing through three points.
176
+
177
+ :param float x1: X-coordinate of the first point
178
+ :param float y1: Y-coordinate of the first point
179
+ :param float x2: X-coordinate of the second point
180
+ :param float y2: Y-coordinate of the second point
181
+ :param float x3: X-coordinate of the third point
182
+ :param float y3: Y-coordinate of the third point
183
+ :returns: Radius of curvature of the circle passing through the three points
184
+ :rtype: float
185
+ """
186
+ numerator = np.sqrt(
187
+ ((x3 - x2) ** 2 + (y3 - y2) ** 2) *
188
+ ((x1 - x3) ** 2 + (y1 - y3) ** 2) *
189
+ ((x2 - x1) ** 2 + (y2 - y1) ** 2)
190
+ )
191
+
192
+ denominator = 2 * abs(
193
+ ((x2 - x1) * (y1 - y3) - (x1 - x3) * (y2 - y1))
194
+ )
195
+
196
+ return numerator / denominator
@@ -4,3 +4,5 @@ from abc import ABC
4
4
  class BaseMotor(ABC):
5
5
  def __init__(self):
6
6
  super().__init__()
7
+
8
+
@@ -1,6 +1,6 @@
1
1
  import math
2
2
  import numpy as np
3
-
3
+ from numpy.typing import NDArray
4
4
  from physics.models.motor.base_motor import BaseMotor
5
5
  from physics.models.constants import ACCELERATION_G, AIR_DENSITY
6
6
 
@@ -33,7 +33,7 @@ class BasicMotor(BaseMotor):
33
33
  # print("torque experienced by motor: {} Nm".format(self.constant_torque))
34
34
 
35
35
  @staticmethod
36
- def calculate_motor_efficiency(motor_angular_speed, motor_output_energy, tick):
36
+ def calculate_motor_efficiency(motor_angular_speed, motor_output_energy, tick, *args, **kwargs):
37
37
  """
38
38
 
39
39
  Calculates a NumPy array of motor efficiency from NumPy array of operating angular speeds and NumPy array
@@ -98,19 +98,20 @@ class BasicMotor(BaseMotor):
98
98
 
99
99
  return e_mc
100
100
 
101
- def calculate_energy_in(self, required_speed_kmh, gradients, wind_speeds, tick):
101
+ def calculate_net_force(self,
102
+ required_speed_kmh: NDArray,
103
+ wind_speeds: NDArray,
104
+ gradients: NDArray
105
+ ) -> tuple[NDArray, NDArray]:
102
106
  """
103
-
104
- Create a function which takes in array of elevation, array of wind speed, required
105
- speed, returns the consumed energy.
106
-
107
- :param np.ndarray required_speed_kmh: (float[N]) required speed array in km/h
108
- :param np.ndarray gradients: (float[N]) gradient at parts of the road
109
- :param np.ndarray wind_speeds: (float[N]) speeds of wind in m/s, where > 0 means against the direction of the vehicle
110
- :param float tick: length of 1 update cycle in seconds
111
- :returns: (float[N]) energy expended by the motor at every tick
112
- :rtype: np.ndarray
113
-
107
+ Calculate the net force on the car, and the required wheel angular velocity.
108
+ Currently, considers:
109
+ 1. Rolling resistance of the wheels on the road
110
+ 2. Drag force (wind + forward velocity)
111
+ 3. Acceleration force (a = F / m)
112
+ 4. Gravitational force (force to go uphill)
113
+
114
+ :return: net force in N, wheel angular velocity in rad/s
114
115
  """
115
116
  required_speed_ms = required_speed_kmh / 3.6
116
117
 
@@ -129,6 +130,24 @@ class BasicMotor(BaseMotor):
129
130
 
130
131
  net_force = road_friction_array + drag_forces + g_forces + acceleration_force
131
132
 
133
+ return net_force, required_angular_speed_rads
134
+
135
+ def calculate_energy_in(self, required_speed_kmh, gradients, wind_speeds, tick, **kwargs):
136
+ """
137
+
138
+ Create a function which takes in array of elevation, array of wind speed, required
139
+ speed, returns the consumed energy.
140
+
141
+ :param np.ndarray required_speed_kmh: (float[N]) required speed array in km/h
142
+ :param np.ndarray gradients: (float[N]) gradient at parts of the road
143
+ :param np.ndarray wind_speeds: (float[N]) speeds of wind in m/s, where > 0 means against the direction of the vehicle
144
+ :param float tick: length of 1 update cycle in seconds
145
+ :returns: (float[N]) energy expended by the motor at every tick
146
+ :rtype: np.ndarray
147
+
148
+ """
149
+ net_force, required_angular_speed_rads = self.calculate_net_force(required_speed_kmh, wind_speeds, gradients)
150
+
132
151
  motor_output_energies = required_angular_speed_rads * net_force * self.tire_radius * tick
133
152
  motor_output_energies = np.clip(motor_output_energies, a_min=0, a_max=None)
134
153