Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 31 additions & 38 deletions .github/workflows/run_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,34 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.12"

- name: Install Poetry
run: |
python -m pip install poetry==1.8.3

- name: Configure Poetry
run: |
python -m poetry config virtualenvs.in-project true

- name: Cache the Virtualenv
uses: actions/cache@v4
with:
path: ./.venv
key: ${{ runner.os }}-venv-${{ hashFiles('**/poetry.lock') }}

- name: Install Dependencies
run: |
python -m poetry lock
python -m poetry install --with test

- name: Run Tests
env:
SOLCAST_API_KEY: ${{ secrets.SOLCAST_API_KEY }}
run: |
python -m poetry run pytest --cov --cov-branch --cov-report=xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}



- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Cache uv
uses: actions/cache@v4
with:
path: |
~/.cache/uv
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock', '**/pyproject.toml') }}

- name: Install Dependencies
run: |
uv sync --group test

- name: Run Tests
env:
SOLCAST_API_KEY: ${{ secrets.SOLCAST_API_KEY }}
run: |
uv run pytest -m "not ci_skip and not solcast" --cov --cov-branch --cov-report=xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
3 changes: 2 additions & 1 deletion MANIFEST.ln
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
static_data/**/*.csv
static_data/**/*.csv
*/**/*.toml
4 changes: 2 additions & 2 deletions data_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .query import ( # noqa: E402
FluxQuery,
FluxStatement,
DBClient,
InfluxDBClient,
PostgresClient,
SunbeamClient
)
Expand Down Expand Up @@ -37,7 +37,7 @@
"FluxQuery",
"FluxStatement",
"TimeSeries",
"DBClient",
"InfluxDBClient",
"FSGPDayLaps",
"FSGPDayLaps",
"collect_lap_data",
Expand Down
9 changes: 6 additions & 3 deletions data_tools/collections/time_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pint.registry import Unit


from data_tools import unit_registry #Important so that different TimeSeries don't experience registry errors
from data_tools import unit_registry # Important so that different TimeSeries don't experience registry errors


class TimeSeries(np.ndarray):
Expand All @@ -17,6 +17,7 @@ class TimeSeries(np.ndarray):

Data is homogenous and evenly-spaced, such that temporal period between subsequent elements is constant.
"""
UnitRegistry = unit_registry

# __new__ and __array_finalize__ are mandatory to ensure that
# `TimeSeries` properly acts like a ndarray when necessary.
Expand Down Expand Up @@ -53,8 +54,6 @@ def __init__(self, input_array,
length: float,
units: Unit | str = None,
meta: dict = None):

self.ureg = unit_registry # Connect TimeSeries to a global registry

# Check if the start and stop are not naive
if start_time is not None:
Expand Down Expand Up @@ -88,6 +87,10 @@ def __init__(self, input_array,

self._meta = meta

@property
def ureg(self):
return TimeSeries.UnitRegistry

def __add__(self, other):
if isinstance(other, TimeSeries):
self_aligned, other_aligned = TimeSeries.align(self, other)
Expand Down
10 changes: 5 additions & 5 deletions data_tools/lap_tools/lap_query.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from data_tools.lap_tools.fsgp_2024_laps import FSGPDayLaps
from data_tools.query.influxdb_query import DBClient
from data_tools.query.influxdb_query import InfluxDBClient
from typing import Callable
import numpy as np

def collect_lap_data(query_func: Callable, client: DBClient, include_day_2=False,
def collect_lap_data(query_func: Callable, client: InfluxDBClient, include_day_2=False,
verbose=False) -> np.ndarray:
"""
Higher order function - computes `query_func` for each lap in FSGP 2024 and returns the resulting array.
Expand All @@ -24,13 +24,13 @@ def get_average_speed(start_time: datetime.datetime, end_time: datetime.datetime
return np.mean(lap_speed)


client = DBClient()
client = InfluxDBClient()

average_speeds = collect_lap_data(get_average_speed, client)
```

:param Callable query_func: must take in parameters (lap_start: datetime, lap_end:datetime, data_client:DBClient)
:param DBClient client: client to use for querying
:param Callable query_func: must take in parameters (lap_start: datetime, lap_end:datetime, data_client:InfluxDBClient)
:param InfluxDBClient client: client to use for querying
:param include_day_2: flag to include the three day 2 laps, driven slowly & under heavy rain
:param verbose: if True, print out queried data during execution
:return: a NumPy ndarray of `query_func` results for all laps
Expand Down
48 changes: 48 additions & 0 deletions data_tools/localization/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from .language_localization import LanguageLocalization as _LanguageLocalization, CanonicalName
from .temporal_localization import TemporalLocalization as _TemporalLocalization
from .spatial_localization import SpatialLocalization as _SpatialLocalization
from .versioned_table import VersionedTable
from .localization import Localization
import pathlib

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the imports neatly ordered by line length



INFLUXDB_LANGUAGE_LOCALIZATION_TABLE_PATH = pathlib.Path(__file__).parent / "influxdb_language_localization.toml"
if not INFLUXDB_LANGUAGE_LOCALIZATION_TABLE_PATH.exists():
raise FileNotFoundError(f"Localization file {INFLUXDB_LANGUAGE_LOCALIZATION_TABLE_PATH} not found! "
f"A localization file is required to use this module")
else:
InfluxDBLanguageLocalization = _LanguageLocalization(INFLUXDB_LANGUAGE_LOCALIZATION_TABLE_PATH)

SUNBEAMDB_LANGUAGE_LOCALIZATION_TABLE_PATH = pathlib.Path(__file__).parent / "sunbeamdb_language_localization.toml"
if not SUNBEAMDB_LANGUAGE_LOCALIZATION_TABLE_PATH.exists():
raise FileNotFoundError(f"Localization file {SUNBEAMDB_LANGUAGE_LOCALIZATION_TABLE_PATH} not found! "
f"A localization file is required to use this module")
else:
SunbeamDBLanguageLocalization = _LanguageLocalization(SUNBEAMDB_LANGUAGE_LOCALIZATION_TABLE_PATH)


TEMPORAL_LOCALIZATION_TABLE_PATH = pathlib.Path(__file__).parent / "temporal_localization.toml"
if not TEMPORAL_LOCALIZATION_TABLE_PATH.exists():
raise FileNotFoundError(f"Localization file {TEMPORAL_LOCALIZATION_TABLE_PATH} not found! "
f"A localization file is required to use this module")
else:
TemporalLocalization = _TemporalLocalization(TEMPORAL_LOCALIZATION_TABLE_PATH)


SPATIAL_LOCALIZATION_TABLE_PATH = pathlib.Path(__file__).parent / "spatial_localization.toml"
if not SPATIAL_LOCALIZATION_TABLE_PATH.exists():
raise FileNotFoundError(f"Localization file {SPATIAL_LOCALIZATION_TABLE_PATH} not found! "
f"A localization file is required to use this module")
else:
SpatialLocalization = _SpatialLocalization(SPATIAL_LOCALIZATION_TABLE_PATH)


__all__ = [
"VersionedTable",
"Localization",
"InfluxDBLanguageLocalization",
"SunbeamDBLanguageLocalization",
"TemporalLocalization",
"SpatialLocalization",
"CanonicalName"
]
81 changes: 81 additions & 0 deletions data_tools/localization/influxdb_language_localization.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[2024-01-01] # 2024-07-12 – 2024-07-20
VehicleSpeed = ["VehicleVelocity", "MDI", "m/s", 5]
PackVoltage = ["TotalPackVoltage", "BMS", "V", 1]
PackCurrent = ["PackCurrent", "ECU", "A", 5]
MotorCurrent = ["BatteryCurrent", "MC", "A", 5]
AcceleratorPosition = ["AcceleratorPosition", "MC", "", 10]
MinimumModuleVoltage = ["VoltageofLeast", "BMS","V", 1]
MaximumModuleVoltage = ["VoltageofHighest", "BMS", "V", 1]
BrakePressed = ["MechBrakePressed", "MCB", "", 10]
MotorCurrentDirection = ["BatteryCurrentDirection", "MC", "", 5]

MPPTInputVoltageA = ["VoltSensor1", "AMB", "V", 4]
MPPTInputVoltageB = ["VoltSensor2", "AMB", "V", 4]
MPPTInputCurrentA = ["CurrentSensor1", "AMB", "A", 4]
MPPTInputCurrentB = ["CurrentSensor2", "AMB", "A", 4]


[2025-01-01] # 2025-04-01 – 2025-07-23
VehicleSpeed = ["MotorRotatingSpeed", "MC", "km/h", 10]
PackVoltage = ["TotalPackVoltage", "BMS", "V", 10]
PackCurrent = ["PackCurrent", "ECU", "A", 5]
MotorCurrent = ["BatteryCurrent", "MC", "A", 10]
AcceleratorPosition = ["AcceleratorPosition", "MC", "", 10]
MinimumModuleVoltage = ["VoltageofLeast", "BMS","V", 10]
MaximumModuleVoltage = ["VoltageofHighest", "BMS", "V", 10]
BrakePressed = ["BrakePressed", "DRD", "", 10]
MotorCurrentDirection = ["BatteryCurrentDirection", "MC", "", 10]

MPPTOutputVoltageA = ["OutputVoltageA", "MPPT", "V", 3]
MPPTOutputVoltageB = ["OutputVoltageB", "MPPT", "V", 3]
MPPTOutputVoltageC = ["OutputVoltageC", "MPPT", "V", 3]
MPPTOutputCurrentA = ["OutputCurrentA", "MPPT", "A", 3]
MPPTOutputCurrentB = ["OutputCurrentB", "MPPT", "A", 3]
MPPTOutputCurrentC = ["OutputCurrentC", "MPPT", "A", 3]

MPPTInputVoltageA = ["InputVoltageA", "MPPT", "V", 3]
MPPTInputVoltageB = ["InputVoltageB", "MPPT", "V", 3]
MPPTInputVoltageC = ["InputVoltageC", "MPPT", "V", 3]
MPPTInputCurrentA = ["InputCurrentA", "MPPT", "A", 3]
MPPTInputCurrentB = ["InputCurrentB", "MPPT", "A", 3]
MPPTInputCurrentC = ["InputCurrentC", "MPPT", "A", 3]

GPSLatitude = ["GPSLatitude", "TEL", "", 3]
GPSLongitude = ["GPSLongitude", "TEL", "", 3]

Acceleration_X = ["Acceleration_X", "TEL", "", 10]
Acceleration_Y = ["Acceleration_Y", "TEL", "", 10]
Acceleration_Z = ["Acceleration_Z", "TEL", "", 10]


[2026-01-01] # 2026-01-20 –
VehicleSpeed = ["MotorRotatingSpeed", "MC", "km/h", 10]
PackVoltage = ["TotalPackVoltage", "BMS", "V", 10]
PackCurrent = ["PackCurrent", "ECU", "A", 5]
MotorCurrent = ["BatteryCurrent", "MC", "A", 10]
AcceleratorPosition = ["AcceleratorPosition", "MC", "", 10]
MinimumModuleVoltage = ["VoltageofLeast", "BMS", "V", 10]
MaximumModuleVoltage = ["VoltageofHighest", "BMS", "V", 10]
BrakePressed = ["BrakePressed", "DRD", "", 10]
MotorCurrentDirection = ["BatteryCurrentDirection", "MC", "", 10]

MPPTOutputVoltageA = ["OutputVoltageA", "MPPT", "V", 3]
MPPTOutputVoltageB = ["OutputVoltageB", "MPPT", "V", 3]
MPPTOutputVoltageC = ["OutputVoltageC", "MPPT", "V", 3]
MPPTOutputCurrentA = ["OutputCurrentA", "MPPT", "A", 3]
MPPTOutputCurrentB = ["OutputCurrentB", "MPPT", "A", 3]
MPPTOutputCurrentC = ["OutputCurrentC", "MPPT", "A", 3]

MPPTInputVoltageA = ["InputVoltageA", "MPPT", "V", 3]
MPPTInputVoltageB = ["InputVoltageB", "MPPT", "V", 3]
MPPTInputVoltageC = ["InputVoltageC", "MPPT", "V", 3]
MPPTInputCurrentA = ["InputCurrentA", "MPPT", "A", 3]
MPPTInputCurrentB = ["InputCurrentB", "MPPT", "A", 3]
MPPTInputCurrentC = ["InputCurrentC", "MPPT", "A", 3]

GPSLatitude = ["GPSLatitude", "TEL", "", 4]
GPSLongitude = ["GPSLongitude", "TEL", "", 4]

Acceleration_X = ["Acceleration_X", "TEL", "", 10]
Acceleration_Y = ["Acceleration_Y", "TEL", "", 10]
Acceleration_Z = ["Acceleration_Z", "TEL", "", 10]
48 changes: 48 additions & 0 deletions data_tools/localization/language_localization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from data_tools.localization.localization import Localization
from enum import StrEnum
from datetime import date


class LanguageLocalization(Localization):
def localize(self, canonical_name: str, current_date: date | str) -> tuple[str, str, str, float]:
"""
Return the (field, board, units, frequency)
"""
name, board, units, frequency = self._localization_table.lookup(canonical_name, current_date)
return name, board, units, frequency


class CanonicalName(StrEnum):
VehicleSpeed = "VehicleSpeed"
PackVoltage = "PackVoltage"
PackCurrent = "PackCurrent"
MotorCurrent = "MotorCurrent"
AcceleratorPosition = "AcceleratorPosition"
MinimumModuleVoltage = "MinimumModuleVoltage"
MaximumModuleVoltage = "MaximumModuleVoltage"
BrakePressed = "BrakePressed"
MotorCurrentDirection = "MotorCurrentDirection"

MPPTOutputVoltageA = "MPPTOutputVoltageA"
MPPTOutputVoltageB = "MPPTOutputVoltageB"
MPPTOutputVoltageC = "MPPTOutputVoltageC"
MPPTOutputCurrentA = "MPPTOutputCurrentA"
MPPTOutputCurrentB = "MPPTOutputCurrentB"
MPPTOutputCurrentC = "MPPTOutputCurrentC"

MPPTInputVoltageA = "MPPTInputVoltageA"
MPPTInputVoltageB = "MPPTInputVoltageB"
MPPTInputVoltageC = "MPPTInputVoltageC"
MPPTInputCurrentA = "MPPTInputCurrentA"
MPPTInputCurrentB = "MPPTInputCurrentB"
MPPTInputCurrentC = "MPPTInputCurrentC"

GPSLatitude = "GPSLatitude"
GPSLongitude = "GPSLongitude"

Acceleration_X = "Acceleration_X"
Acceleration_Y = "Acceleration_Y"
Acceleration_Z = "Acceleration_Z"

MotorPower = "MotorPower"
MotorEfficiency = "MotorEfficiency"
13 changes: 13 additions & 0 deletions data_tools/localization/localization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from data_tools.localization.versioned_table import VersionedTable
from abc import ABC, abstractmethod
import pathlib


class Localization(ABC):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a brief docstring describing the purpose of the class. Same goes for the other new classes.

_instance: "Localization | None" = None

def __init__(self, localization_table: pathlib.Path):
self._localization_table = VersionedTable.from_toml_file(str(localization_table.absolute()))

@abstractmethod
def localize(self, *args, **kwargs): ...
13 changes: 13 additions & 0 deletions data_tools/localization/spatial_localization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from data_tools.localization.localization import Localization
from datetime import datetime


class SpatialLocalization(Localization):
def localize(self, query_datetime: datetime) -> tuple[list, str]:
"""
Return the timedelta to add to InfluxDB query ranges to account for timezone reporting errors in InfluxDB.
The same error should be subtracted from returned data ranges to amend them.
"""
coords, name = self._localization_table.lookup("position", query_datetime.date())

return coords, name
Loading
Loading