diff --git a/CHANGES_SUMMARY.md b/CHANGES_SUMMARY.md new file mode 100644 index 0000000..fa1c5c3 --- /dev/null +++ b/CHANGES_SUMMARY.md @@ -0,0 +1,77 @@ +# Summary of Changes + +## Overview +Successfully restructured the PIT library to enable the import notation: +```python +import pit.dynamics.singletrack as st +``` + +Users can now access both dynamics models and loss functions through the unified `st.` namespace. + +## Changes Made + +### 1. Created New Package Structure +- **Created directory**: `pit/dynamics/singletrack/` +- **Created module**: `pit/dynamics/singletrack/dynamics.py` (copied from `pit/dynamics/single_track.py`) +- **Created module**: `pit/dynamics/singletrack/loss.py` (copied from `pit/utilities/loss.py`) +- **Created package**: `pit/dynamics/singletrack/__init__.py` to export all components + +### 2. Package Exports +The `pit.dynamics.singletrack` package now exports: +- **Classes**: `SingleTrack`, `SingleTrackMod` +- **Loss Functions**: `yaw_normalized_loss`, `yaw_normalized_loss_per_item`, `yaw_normalized_loss_per_element` +- **Constants**: `ANGLE_INDICES`, `non_angle_indices` + +### 3. Updated Notebooks +Modified the following notebooks to use the new import structure: +- `bin/ModelFitting.ipynb` +- `bin/AWSIM_Model_Fitting.ipynb` + +Both notebooks now import with: +```python +import pit.dynamics.singletrack as st +SingleTrack = st.SingleTrack # For backward compatibility +``` + +### 4. Added Documentation and Examples +- **IMPORT_GUIDE.md**: Complete guide on using the new import structure +- **example_new_imports.py**: Example demonstrating the new imports +- **test_imports.py**: Validation script for the new structure + +## Usage Examples + +### Before (Old Style): +```python +from pit.dynamics.single_track import SingleTrack +from pit.utilities.loss import yaw_normalized_loss + +model = SingleTrack(...) +loss = yaw_normalized_loss(output, target) +``` + +### After (New Style): +```python +import pit.dynamics.singletrack as st + +model = st.SingleTrack(...) +loss = st.yaw_normalized_loss(output, target) +``` + +## Backward Compatibility +✓ Old import paths still work +✓ Existing code remains functional +✓ No breaking changes introduced + +## Key Features +✓ Simple and minimal structure +✓ No unnecessary error handling +✓ Clean unified namespace +✓ Easy to use: `st.yaw_normalized_loss`, `st.SingleTrack`, etc. + +## Testing +All structural validations passed: +- Package directory exists ✓ +- All required modules present ✓ +- All exports available ✓ +- Notebooks updated correctly ✓ +- Documentation complete ✓ diff --git a/IMPORT_GUIDE.md b/IMPORT_GUIDE.md new file mode 100644 index 0000000..9fc91e2 --- /dev/null +++ b/IMPORT_GUIDE.md @@ -0,0 +1,49 @@ +# Using the New Import Structure + +The PIT library has been reorganized to allow importing single track dynamics and loss functions together under a unified namespace. + +## New Import Style + +```python +import pit.dynamics.singletrack as st + +# Access dynamics classes +model = st.SingleTrack(m=1225, Iz=1538, lf=0.88, lr=1.51, hcg=0.5, Csf=4.5, Csr=5.2, mu=0.9) +model_mod = st.SingleTrackMod(m=1225, Iz=1538, lf=0.88, lr=1.51, hcg=0.5, Csf=4.5, Csr=5.2, mu=0.9) + +# Access loss functions +loss = st.yaw_normalized_loss(output_states, target_states) +loss_per_item = st.yaw_normalized_loss_per_item(output_states, target_states) +loss_per_element = st.yaw_normalized_loss_per_element(output_states, target_states) + +# Access constants +angle_indices = st.ANGLE_INDICES # [4] +non_angle = st.non_angle_indices # [0, 1, 2, 3, 5, 6] +``` + +## Available Exports + +The `pit.dynamics.singletrack` module exports: + +- **Dynamics Classes:** + - `SingleTrack` - Standard single track vehicle dynamics model + - `SingleTrackMod` - Modified single track vehicle dynamics model + +- **Loss Functions:** + - `yaw_normalized_loss` - Normalized loss with special handling for yaw angles + - `yaw_normalized_loss_per_item` - Per-batch-item normalized loss + - `yaw_normalized_loss_per_element` - Per-element normalized loss + +- **Constants:** + - `ANGLE_INDICES` - Indices of angular state variables + - `non_angle_indices` - Indices of non-angular state variables + +## Backward Compatibility + +The old import paths still work: +```python +from pit.dynamics.single_track import SingleTrack +from pit.utilities.loss import yaw_normalized_loss +``` + +However, the new unified import is preferred for cleaner code organization. diff --git a/bin/AWSIM_Model_Fitting.ipynb b/bin/AWSIM_Model_Fitting.ipynb index b8ccad8..f52253c 100644 --- a/bin/AWSIM_Model_Fitting.ipynb +++ b/bin/AWSIM_Model_Fitting.ipynb @@ -21,7 +21,8 @@ "import numpy as np\n", "\n", "from pit.dynamics.dynamic_bicycle import DynamicBicycle\n", - "from pit.dynamics.single_track import SingleTrack\n", + "import pit.dynamics.singletrack as st\n", + "SingleTrack = st.SingleTrack\n", "from pit.parameters import NormalParameterGroup, CovariantNormalParameterGroup, PointParameterGroup\n", "from pit.integration import Euler, RK4\n", "\n", @@ -824,4 +825,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/bin/ModelFitting.ipynb b/bin/ModelFitting.ipynb index 754d995..c7904db 100644 --- a/bin/ModelFitting.ipynb +++ b/bin/ModelFitting.ipynb @@ -21,7 +21,8 @@ "import numpy as np\n", "\n", "from pit.dynamics.dynamic_bicycle import DynamicBicycle\n", - "from pit.dynamics.single_track import SingleTrack\n", + "import pit.dynamics.singletrack as st\n", + "SingleTrack = st.SingleTrack\n", "from pit.parameters import NormalParameterGroup, CovariantNormalParameterGroup, PointParameterGroup\n", "from pit.integration import Euler, RK4\n", "\n", @@ -726,4 +727,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/example_new_imports.py b/example_new_imports.py new file mode 100644 index 0000000..ea4c8fc --- /dev/null +++ b/example_new_imports.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Example demonstrating the new import structure for pit.dynamics.singletrack + +This example shows how to use the new unified import to access both +dynamics models and loss functions. +""" + +import pit.dynamics.singletrack as st + +def main(): + print("=" * 70) + print("PIT Single Track - New Import Structure Example") + print("=" * 70) + + # Show available exports + print("\n1. Available Classes and Functions:") + print(" Classes:") + print(f" - st.SingleTrack: {st.SingleTrack}") + print(f" - st.SingleTrackMod: {st.SingleTrackMod}") + + print("\n Loss Functions:") + print(f" - st.yaw_normalized_loss: {st.yaw_normalized_loss}") + print(f" - st.yaw_normalized_loss_per_item: {st.yaw_normalized_loss_per_item}") + print(f" - st.yaw_normalized_loss_per_element: {st.yaw_normalized_loss_per_element}") + + print("\n Constants:") + print(f" - st.ANGLE_INDICES: {st.ANGLE_INDICES}") + print(f" - st.non_angle_indices: {st.non_angle_indices}") + + # Example: Create a SingleTrack model + print("\n2. Creating a SingleTrack model:") + print(" model = st.SingleTrack(") + print(" m=1225.887,") + print(" Iz=1538.853,") + print(" lf=0.88392,") + print(" lr=1.50876,") + print(" hcg=0.5,") + print(" Csf=4.5,") + print(" Csr=5.2,") + print(" mu=0.9") + print(" )") + + # Show usage with loss + print("\n3. Using loss functions:") + print(" # After integrating dynamics to get output_states") + print(" loss = st.yaw_normalized_loss(output_states, target_states)") + print(" loss_per_item = st.yaw_normalized_loss_per_item(output_states, target_states)") + + print("\n" + "=" * 70) + print("✓ Import structure validated successfully!") + print("=" * 70) + +if __name__ == "__main__": + main() diff --git a/pit/dynamics/singletrack/__init__.py b/pit/dynamics/singletrack/__init__.py new file mode 100644 index 0000000..78d2be3 --- /dev/null +++ b/pit/dynamics/singletrack/__init__.py @@ -0,0 +1,18 @@ +from .dynamics import SingleTrack, SingleTrackMod +from .loss import ( + yaw_normalized_loss, + yaw_normalized_loss_per_item, + yaw_normalized_loss_per_element, + ANGLE_INDICES, + non_angle_indices, +) + +__all__ = [ + "SingleTrack", + "SingleTrackMod", + "yaw_normalized_loss", + "yaw_normalized_loss_per_item", + "yaw_normalized_loss_per_element", + "ANGLE_INDICES", + "non_angle_indices", +] diff --git a/pit/dynamics/singletrack/dynamics.py b/pit/dynamics/singletrack/dynamics.py new file mode 100644 index 0000000..aff704d --- /dev/null +++ b/pit/dynamics/singletrack/dynamics.py @@ -0,0 +1,161 @@ +from .. import Dynamics +from ...parameters.definitions import ParameterSample + +import torch +from torch import nn + + +class SingleTrack(Dynamics, nn.Module): + """ + This is the Single Track model, from the CommonRoad paper. + Link: https://gitlab.lrz.de/tum-cps/commonroad-vehicle-models/-/blob/master/vehicleModels_commonRoad.pdf + """ + + def __init__(self, m, Iz, lf, lr, hcg, Csf, Csr, mu, **kwargs) -> None: + super().__init__() + self.parameter_list = ["m", "Iz", "lf", "lr", "hcg", "Csf", "Csr", "mu"] + self.initial_values = { + "m": m, + "Iz": Iz, + "lf": lf, + "lr": lr, + "hcg": hcg, + "Csf": Csf, + "Csr": Csr, + "mu": mu, + } + self.g = 9.81 + self.numeric_stability_constant = 1e-10 + + def forward(self, states, control_inputs, params: ParameterSample): + """Get the evaluated ODEs of the state at this point + + Args: + states (): Shape of (B, 7) or (7) + [X, Y, STEER, V, YAW, YAW_RATE, SLIP_ANGLE] + control_inputs (): Shape of (B, 2) or (2) + [STEER_V, ACCEL] + """ + X, Y, STEER, V, YAW, YAW_RATE, SLIP_ANGLE = 0, 1, 2, 3, 4, 5, 6 + STEER_V, ACCEL = 0, 1 + diff = torch.zeros_like(states) + diff[..., X] = states[..., V] * torch.cos( + states[..., YAW] + states[..., SLIP_ANGLE] + ) + diff[..., Y] = states[..., V] * torch.sin( + states[..., YAW] + states[..., SLIP_ANGLE] + ) + diff[..., STEER] = control_inputs[..., STEER_V] + diff[..., YAW] = states[..., YAW_RATE] + diff[..., V] = control_inputs[..., ACCEL] + glr = self.g * params["lr"] - control_inputs[..., ACCEL] * params["hcg"] + glf = self.g * params["lf"] + control_inputs[..., ACCEL] * params["hcg"] + diff[..., YAW_RATE] = ( + (params["mu"] * params["m"]) + / (params["Iz"] * (params["lf"] + params["lr"])) + ) * ( + params["lf"] * params["Csf"] * glr * states[..., STEER] + + (params["lr"] * params["Csr"] * glf - params["lf"] * params["Csf"] * glr) + * states[..., SLIP_ANGLE] + - ( + params["lf"] * params["lf"] * params["Csf"] * glr + + params["lr"] * params["lr"] * params["Csr"] * glf + ) + * ( + states[..., YAW_RATE] + / (self.numeric_stability_constant + states[..., V]) + ) + ) + + diff[..., SLIP_ANGLE] = ( + params["mu"] / (states[..., V] * (params["lr"] + params["lf"])) + ) * ( + params["Csf"] * glr * states[..., STEER] + - (params["Csr"] * glf + params["Csf"] * glr) * states[..., SLIP_ANGLE] + + (params["Csr"] * glf * params["lr"] - params["Csf"] * glr * params["lf"]) + * ( + states[..., YAW_RATE] + / (self.numeric_stability_constant + states[..., V]) + ) + ) - states[..., YAW_RATE] + + return diff + + +class SingleTrackMod(Dynamics, nn.Module): + """ + This is the Single Track model, from the CommonRoad paper. + Link: https://gitlab.lrz.de/tum-cps/commonroad-vehicle-models/-/blob/master/vehicleModels_commonRoad.pdf + """ + + def __init__(self, m, Iz, lf, lr, hcg, Csf, Csr, mu, **kwargs) -> None: + super().__init__() + self.parameter_list = ["m", "Iz", "lf", "lr", "hcg", "Csf", "Csr", "mu"] + self.initial_values = { + "m": m, + "Iz": Iz, + "lf": lf, + "lr": lr, + "hcg": hcg, + "Csf": Csf, + "Csr": Csr, + "mu": mu, + } + self.g = 9.81 + self.numeric_stability_constant = 1e-10 + + def forward(self, states, control_inputs, params: ParameterSample): + """Get the evaluated ODEs of the state at this point + + Args: + states (): Shape of (B, 6) or (6) + [X, Y, V, YAW, YAW_RATE, SLIP_ANGLE] + control_inputs (): Shape of (B, 2) or (2) + [STEER_ANGLE, ACCEL] + """ + X, Y, V, YAW, YAW_RATE, SLIP_ANGLE = 0, 1, 2, 3, 4, 5 + CONTROL_STEER_ANGLE, ACCEL = 0, 1 + diff = torch.zeros_like(states) + diff[..., X] = states[..., V] * torch.cos( + states[..., YAW] + states[..., SLIP_ANGLE] + ) + diff[..., Y] = states[..., V] * torch.sin( + states[..., YAW] + states[..., SLIP_ANGLE] + ) + diff[..., YAW] = states[..., YAW_RATE] + diff[..., V] = control_inputs[..., ACCEL] + glr = self.g * params["lr"] - control_inputs[..., ACCEL] * params["hcg"] + glf = self.g * params["lf"] + control_inputs[..., ACCEL] * params["hcg"] + diff[..., YAW_RATE] = ( + (params["mu"] * params["m"]) + / (params["Iz"] * (params["lf"] + params["lr"])) + ) * ( + params["lf"] + * params["Csf"] + * glr + * control_inputs[..., CONTROL_STEER_ANGLE] + + (params["lr"] * params["Csr"] * glf - params["lf"] * params["Csf"] * glr) + * states[..., SLIP_ANGLE] + - ( + params["lf"] * params["lf"] * params["Csf"] * glr + + params["lr"] * params["lr"] * params["Csr"] * glf + ) + * ( + states[..., YAW_RATE] + / (self.numeric_stability_constant + states[..., V]) + ) + ) + + diff[..., SLIP_ANGLE] = ( + params["mu"] / (states[..., V] * (params["lr"] + params["lf"])) + ) * ( + params["Csf"] * glr * control_inputs[..., CONTROL_STEER_ANGLE] + - (params["Csr"] * glf + params["Csf"] * glr) * states[..., SLIP_ANGLE] + + (params["Csr"] * glf * params["lr"] - params["Csf"] * glr * params["lf"]) + * ( + states[..., YAW_RATE] + / (self.numeric_stability_constant + states[..., V]) + ) + ) - states[..., YAW_RATE] + + return diff diff --git a/pit/dynamics/singletrack/loss.py b/pit/dynamics/singletrack/loss.py new file mode 100644 index 0000000..613ed0c --- /dev/null +++ b/pit/dynamics/singletrack/loss.py @@ -0,0 +1,73 @@ +import torch + +ANGLE_INDICES = [4] +all_indices = list(range(7)) +non_angle_indices = [i for i in all_indices if i not in ANGLE_INDICES] + + +def yaw_normalized_loss(output_states, target_states): + normal_loss = torch.nn.functional.l1_loss( + output_states[..., non_angle_indices], + target_states[..., non_angle_indices], + reduction="sum", + ) + angular_loss = torch.sum( + torch.abs( + torch.atan2( + torch.sin( + output_states[..., ANGLE_INDICES] + - target_states[..., ANGLE_INDICES] + ), + torch.cos( + output_states[..., ANGLE_INDICES] + - target_states[..., ANGLE_INDICES] + ), + ) + ) + ) + return normal_loss + angular_loss + + +def yaw_normalized_loss_per_item(output_states, target_states): + """Calculate the yaw normalized loss per item in the batch""" + normal_loss_p1 = torch.nn.functional.l1_loss( + output_states[..., non_angle_indices], + target_states[..., non_angle_indices], + reduction="none", + ).sum([1, 2]) + angular_loss = torch.abs( + torch.atan2( + torch.sin( + output_states[..., ANGLE_INDICES] - target_states[..., ANGLE_INDICES] + ), + torch.cos( + output_states[..., ANGLE_INDICES] - target_states[..., ANGLE_INDICES] + ), + ) + ).sum([1]) + return normal_loss_p1 + angular_loss + + +def yaw_normalized_loss_per_element(output_states, target_states): + """Assuming the data is (B, T, D), return the loss per dimension in the state, (B,D)""" + losses = torch.zeros( + output_states.shape[0], output_states.shape[2], device=output_states.device + ) + normal_loss = torch.nn.functional.l1_loss( + output_states[..., non_angle_indices], + target_states[..., non_angle_indices], + reduction="none", + ).sum(1) + angular_loss = torch.abs( + torch.atan2( + torch.sin( + output_states[..., ANGLE_INDICES] - target_states[..., ANGLE_INDICES] + ), + torch.cos( + output_states[..., ANGLE_INDICES] - target_states[..., ANGLE_INDICES] + ), + ) + ).sum(1) + losses[..., non_angle_indices] = normal_loss + losses[..., ANGLE_INDICES] = angular_loss + return losses diff --git a/test_imports.py b/test_imports.py new file mode 100644 index 0000000..c90fdcb --- /dev/null +++ b/test_imports.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +""" +Test script to verify the new import structure works correctly. +This demonstrates the desired import notation: import pit.dynamics.singletrack as st +""" + +# Test importing singletrack as st +import pit.dynamics.singletrack as st + +# Test that we can access the dynamics classes +print("SingleTrack class:", st.SingleTrack) +print("SingleTrackMod class:", st.SingleTrackMod) + +# Test that we can access the loss functions +print("yaw_normalized_loss function:", st.yaw_normalized_loss) +print("yaw_normalized_loss_per_item function:", st.yaw_normalized_loss_per_item) +print("yaw_normalized_loss_per_element function:", st.yaw_normalized_loss_per_element) + +# Test that we can access constants +print("ANGLE_INDICES:", st.ANGLE_INDICES) +print("non_angle_indices:", st.non_angle_indices) + +print("\n✓ All imports successful! The new structure works as expected.")