diff --git a/README.md b/README.md index a25c35a..82fcf13 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ uv publish # make sure your version in pyproject.toml is updated or -Update the version number in pyproject.toml and imu_module/__init__.py +Update the version number in pyproject.toml and hip_controller/__init__.py Commit your changes and add a git tag v Push the tag git push --tag @@ -128,18 +128,7 @@ docker build -t hip-controller . docker run hip-controller ``` -## How to Import -Import the main module: -```python -from hip_controller import definitions -``` - -Import specific components: -```python -from hip_controller.control.app import AppController -from hip_controller.control.signal_processing.kalman_filter import KalmanFilter -from hip_controller.plotter.simulator import Simulator ``` ## Architecture and Flow @@ -161,12 +150,13 @@ The main entry point is `app.py` in the control module, which orchestrates the f Run the main application: ```python from hip_controller.control.app import WalkOnController -from hip_controller.definitions import SensorSignal -logger.info("Initializing the lower limb controller.") +from hip_controller.definitions import SensorSignal, BasicConfig +logger.info("Initializing the lower limb controller.") -self.controller_left = WalkOnController(reverse=False, plot=False) -self.controller_right = WalkOnController(reverse=True, plot=False) +config = BasicConfig(filtered=False) +self.controller_left = WalkOnController(left_limb=True, config=config) +self.controller_right = WalkOnController(left_limb=False, config=config) while True: signal_left = SensorSignal(timestamp=timestamp_left, angle_rad=data_left.quat.to_euler(seq="xyz").z, velocity_rad_per_sec=data_left.device_data.gyro.z) @@ -314,6 +304,9 @@ motor_command = limb_controller.step(signal) The following tree shows the important permanent files. Run `make tree` to update. ``` +├── .claude +│ └── skills +│ └── code-review-nathalie.md ├── data │ ├── evaluation_raw_data │ │ ├── incline_walk @@ -793,7 +786,6 @@ The following tree shows the important permanent files. Run `make tree` to updat │ │ ├── AB11_turn_and_step_1_right-turn_angle.csv │ │ ├── AB12_turn_and_step_1_right-turn_angle.csv │ │ └── AB13_turn_and_step_1_right-turn_angle.csv -│ ├── logs │ └── sensor_data │ ├── arduino2_2026_03_23.csv │ ├── arduino_2026_03_23.csv @@ -809,17 +801,17 @@ The following tree shows the important permanent files. Run `make tree` to updat │ ├── compare_all.py │ ├── compare_matlab.py │ ├── compare_matlab_module.py +│ ├── controller_simulator.py │ ├── csv_converter.py -│ ├── csv_utils.py +│ ├── csv_player.py │ ├── evaluation_matplotlib.py │ ├── evaluation_record.py │ ├── live_comparison_plot.py -│ ├── main.py │ ├── mat_to_csv.py │ ├── normalize_output_time.py -│ ├── reference_versus_calculated.py -│ ├── script.py -│ └── simulator.py +│ ├── plot_scenarios.py +│ ├── readme.md +│ └── reference_versus_calculated.py ├── src │ └── hip_controller │ ├── control @@ -834,6 +826,7 @@ The following tree shows the important permanent files. Run `make tree` to updat │ │ │ └── pid_controller.py │ │ ├── signal_processing │ │ │ ├── drift_removal.py +│ │ │ ├── filtering.py │ │ │ ├── sensor_preprocessor.py │ │ │ └── velocity_estimation.py │ │ ├── __init__.py @@ -848,6 +841,7 @@ The following tree shows the important permanent files. Run `make tree` to updat │ │ ├── csv_player.py │ │ └── live_phase_portrait.py │ ├── utils +│ │ ├── csv_utils.py │ │ ├── math_utils.py │ │ ├── state_space.py │ │ └── utils.py @@ -892,6 +886,7 @@ The following tree shows the important permanent files. Run `make tree` to updat ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version +├── CLAUDE.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE diff --git a/pyproject.toml b/pyproject.toml index 8bc223d..6dd5ac1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hip_controller" -version = "0.1.1" +version = "0.1.2" description = "Lower limb exosuit hip controller." readme = "README.md" authors = [ diff --git a/scripts/readme.md b/scripts/readme.md new file mode 100644 index 0000000..e4c6fef --- /dev/null +++ b/scripts/readme.md @@ -0,0 +1,124 @@ +# Scripts Folder + +This document describes the purpose of each script in `scripts/` and how they are intended to be used. + + +## Script descriptions + +### `compare_all.py` +A comparison utility for MATLAB reference data and module output data. It parses matched MATLAB/output CSV pairs, computes RMSE/MAPE metrics, and generates comparison plots. + +### `compare_matlab.py` +Same purpose as `compare_all.py`: compare Simulink/MATLAB CSV data against module output data. Use it to inspect phase and motor command agreement between MATLAB and Python results. + +### `compare_matlab_module.py` +A second comparison helper with the same MATLAB vs module output workflow. It is also intended for RMSE/MAPE calculation and plotting of matched CSV pairs. + +### `controller_simulator.py` +A GUI-focused simulation script. It can replay CSV data through the controller and display live comparison plots. Key functions: +- `simulate_controller_with_data(...)` for full controller playback +- `simulate_comparison_dynamic(...)` for comparing actual output with expected output + +### `csv_converter.py` +CSV processing utilities for walking data. Primary helpers: +- `concatenate_stairs(...)` to merge stair trial CSVs per participant +- `combine_two_files(...)` to join two CSVs while keeping the selected columns + +### `csv_player.py` +A stateful CSV player for real-time-style playback. It loads a CSV once and returns one row at a time so GUI/plotting components can simulate sensor streaming. + +### `evaluation_matplotlib.py` +Plotting utilities for evaluation output. It contains functions to draw gait phase, filtered signals, amplitude, and motor command figures from evaluation CSV files. + +### `evaluation_record.py` +The evaluation preprocessing pipeline. It reads raw sensor CSVs from `data/evaluation_raw_data/`, downsamples and converts them, and writes the resulting evaluation CSV files under `scripts/evaluation_output/`. + +### `live_comparison_plot.py` +A PyQt6 plot window for live signal comparison. Used by simulator components to show input, actual output, and expected output in real time. + +### `mat_to_csv.py` +MAT file conversion helper. It extracts `left` and `right` arrays from `.mat` files and writes them as CSV, preserving folder structure. + +### `normalize_output_time.py` +Normalizes output CSV time columns so `time (s)` starts at `0.00` and increments by `0.01`. Useful for preparing evaluation/output files for plotting or comparison. + +### `plot_scenarios.py` +A comparison plot module for preprocessing validation. It replays CSV rows through a callable, then plots actual vs expected output and residuals in a 3-panel figure. + +### `reference_versus_calculated.py` +A gait-phase comparison tool. It builds a reference phase signal from left/right cycle boundaries and compares it against calculated gait phase output, including RMSE reporting and plots. + +## Usage notes + +- Many scripts are library-style and are best used by importing their functions from Python. +- `evaluation_record.py`, `mat_to_csv.py`, and `normalize_output_time.py` include `__main__` runners for direct execution. +- Visualization scripts generally require `matplotlib`, and GUI scripts require `PyQt6`/`pyqtgraph`. + +For exact call patterns, open the corresponding script and inspect the top-level functions or the `if __name__ == '__main__'` section. + +## Examples + +### Run directly from the shell + +```bash +python scripts/evaluation_record.py +python scripts/mat_to_csv.py +python scripts/normalize_output_time.py +``` + +### Compare MATLAB vs module CSV outputs + +```python +from scripts.compare_matlab import parse_matlab_module, compute_metrics +from pathlib import Path + +matlab_csv = Path('data/evaluation_raw_data/matlab_ref.csv') +output_csv = Path('data/evaluation_output/module_out.csv') +comparison = parse_matlab_module(matlab_csv, output_csv) +metrics = compute_metrics(comparison) +``` + +### Use the CSV player for real-time-style replay + +```python +from scripts.csv_player import ScriptPlayer + +player = ScriptPlayer(Path('data/evaluation_raw_data/some_walk.csv')) +while player.has_next_line(): + row = player.get_data_from_csv('angle_right (rad)', 'gait_phase_right (rad)') +``` + +### Build a preprocessing comparison plot + +```python +from scripts.plot_scenarios import plot_preprocessor_comparison +from hip_controller.definitions import SensorSignal +from hip_controller.control.signal_processing.sensor_preprocessor import SensorPreprocessor, PreprocessorConfig + +preprocessor = SensorPreprocessor(PreprocessorConfig()) +plot_preprocessor_comparison( + csv_path='scripts/evaluation_output/normal_walk/AB01_normal_walk.csv', + time_col='time (s)', + input_col='angle_right (rad)', + expected_output_col='filtered_velocity_right (rad/s)', + build_signal=lambda t, a: SensorSignal(timestamp=t, angle_rad=a, velocity_rad_per_sec=0.0), + run_callable=preprocessor.filter, + extract_output=lambda sig: sig.velocity_rad_per_sec, +) +``` + +### Simulate controller playback + +```python +from scripts.controller_simulator import simulate_controller_with_data +from pathlib import Path + +simulate_controller_with_data(csv_path=Path('data/evaluation_raw_data/normal_walk/AB01_normal_walk.csv')) +``` + +### Normalize evaluation output time values + +```python +from scripts.normalize_output_time import normalize_output_folder +normalize_output_folder('scripts/evaluation_output', 'scripts/normalized_output') +``` diff --git a/src/hip_controller/__init__.py b/src/hip_controller/__init__.py index f05a609..ea69e50 100644 --- a/src/hip_controller/__init__.py +++ b/src/hip_controller/__init__.py @@ -14,7 +14,9 @@ try: import tomli as tomllib except ImportError as err: - raise ImportError("Python 3.10 requires the 'tomli' package: pip install tomli") from err + raise ImportError( + "Python 3.10 requires the 'tomli' package: pip install tomli" + ) from err from importlib.metadata import PackageNotFoundError, version from pathlib import Path diff --git a/src/hip_controller/__main__.py b/src/hip_controller/__main__.py index 519f624..4b4068c 100644 --- a/src/hip_controller/__main__.py +++ b/src/hip_controller/__main__.py @@ -41,8 +41,10 @@ def main( app = QtWidgets.QApplication([]) player = CSVPlayer(csv_path) - controller_left = WalkOnController(reverse=True, plot=True, filtered=True) - controller_right = WalkOnController(reverse=False, plot=True, filtered=True) + config = BasicConfig(filtered=True) + + controller_left = WalkOnController(left_limb=True, config=config) + controller_right = WalkOnController(left_limb=False, config=config) timer = QtCore.QTimer() def update() -> None: diff --git a/src/hip_controller/control/app.py b/src/hip_controller/control/app.py index 1d45903..43d4c6a 100644 --- a/src/hip_controller/control/app.py +++ b/src/hip_controller/control/app.py @@ -10,7 +10,7 @@ from hip_controller.control.signal_processing.sensor_preprocessor import ( SensorPreprocessor, ) -from hip_controller.definitions import PreprocessorConfig, SensorSignal +from hip_controller.definitions import BasicConfig, SensorSignal class WalkOnController: @@ -19,29 +19,38 @@ class WalkOnController: This controller implements a gait phase-based control strategy for a single limb, which can be used for both unilateral and bilateral hip flexion exosuits. The controller processes raw sensor signals to compute the current gait phase, applies amplitude modulation based on the sensor signals, and generates motor velocity commands for the exosuit's actuators. """ - def __init__(self, reverse: bool, plot: bool = False, filtered=False): + def __init__(self, left_limb: bool, config: BasicConfig): """Initialize the controller. - :param bool reverse: Whether to reverse the motor command output (for mirrored wiring). - :param bool plot: Whether to enable live plotting of the controller's internal states. - :param bool filtered: Whether to use pre-filtered sensor signals instead of raw signals. + :param bool left_limb: True if the controller is for left lower limb, False if for right lower limb. + :param BasicConfig config: Configurations including whether to reverse the motor command output (for mirrored wiring), whether to enable live plotting of the controller's internal states, whether to use pre-filtered sensor signals instead of raw signals and so on. :return: None """ - self.plot = plot - self.filtered = filtered - if plot: + self.filtered = config.filtered + if left_limb: + self.plot = config.left_limb_plot + self.amplitude_modulation = AmplitudeModulation( + reverse=config.left_limb_reverse + ) + else: + self.plot = config.right_limb_plot + self.amplitude_modulation = AmplitudeModulation( + reverse=config.right_limb_reverse + ) + + if self.plot: from hip_controller.plotter.live_phase_portrait import PortraitWindow # Execute the Qt plot application. - self.plotter = PortraitWindow(left=not reverse) + self.plotter = PortraitWindow(left=left_limb) self.plotter.show() - self.pre_processor = SensorPreprocessor(PreprocessorConfig()) + self.pre_processor = SensorPreprocessor(basic_config=config) self.gait_controller = GaitController() # due to different wire settings one of them might need to be reversed - mirrored with -1 - self.amplitude_modulation = AmplitudeModulation(reverse=reverse) + self.motion_reference_controller = MotionReferenceController() self._prev_timestamp: float | None = None diff --git a/src/hip_controller/control/signal_processing/drift_removal.py b/src/hip_controller/control/signal_processing/drift_removal.py index 46660cf..a2721b2 100644 --- a/src/hip_controller/control/signal_processing/drift_removal.py +++ b/src/hip_controller/control/signal_processing/drift_removal.py @@ -11,9 +11,7 @@ from hip_controller.definitions import LowPassFilterConfig, NotchConfig from hip_controller.filters.notch_filter import NotchFilter -from hip_controller.filters.second_order_low_pass_filter import ( - SecondOrderLowPassFilter, -) +from hip_controller.filters.second_order_low_pass_filter import SecondOrderLowPassFilter class DriftRemovalStrategy(ABC): diff --git a/src/hip_controller/control/signal_processing/sensor_preprocessor.py b/src/hip_controller/control/signal_processing/sensor_preprocessor.py index 509c856..39aa0ed 100644 --- a/src/hip_controller/control/signal_processing/sensor_preprocessor.py +++ b/src/hip_controller/control/signal_processing/sensor_preprocessor.py @@ -9,17 +9,32 @@ from __future__ import annotations +from loguru import logger + from hip_controller.control.signal_processing.drift_removal import ( DriftRemovalStrategy, + LowPassDriftRemoval, + NotchDriftRemoval, ) from hip_controller.control.signal_processing.filtering import ( FilteringStrategy, + LowPassFiltering, SogiFllFiltering, ) from hip_controller.control.signal_processing.velocity_estimation import ( + DiscreteDerivativeVelocityEstimation, + GyroscopeVelocityEstimation, + LowPassVelocityEstimation, VelocityEstimationStrategy, ) -from hip_controller.definitions import PreprocessorConfig, SensorSignal +from hip_controller.definitions import ( + BasicConfig, + DriftRemovalMethod, + FilteringMethod, + PreprocessorConfig, + SensorSignal, + VelocityEstimationMethod, +) class SensorPreprocessor: @@ -31,23 +46,22 @@ class SensorPreprocessor: """ - def __init__(self, config: PreprocessorConfig) -> None: + def __init__(self, basic_config: BasicConfig) -> None: """Initialize the sensor pre-processor. :param PreprocessorConfig config: Preprocessor configuration. :return: None """ - self.config = config - self._drift_removal: DriftRemovalStrategy = config.drift_removal_strategy - self._sogi_fll: FilteringStrategy = SogiFllFiltering( - config=config.filtering_sogifll_config - ) - self._velocity_estimation: VelocityEstimationStrategy = ( - config.velocity_estimation_strategy - ) + self._basic_config: BasicConfig = basic_config + + self._drift_removal: DriftRemovalStrategy + self._filtering: FilteringStrategy + self._velocity_estimation: VelocityEstimationStrategy self._prev_timestamp: float | None = None + self.__init_strategies__() + def filter(self, raw_signal: SensorSignal) -> SensorSignal: """Run one preprocessing step and return a :class:`SensorSignal`. @@ -65,9 +79,7 @@ def filter(self, raw_signal: SensorSignal) -> SensorSignal: # check dt too big if time_difference > 1.0: - self._drift_removal = self.config.drift_removal_strategy - self._velocity_estimation = self.config.velocity_estimation_strategy - time_difference = 0.01 + self.reset() self._prev_timestamp = raw_signal.timestamp @@ -75,7 +87,7 @@ def filter(self, raw_signal: SensorSignal) -> SensorSignal: raw_angle=raw_signal.angle_rad, time_difference=time_difference ) - angle_out_rad = self._sogi_fll.filter( + angle_out_rad = self._filtering.filter( angle_rad=angle_no_drift_rad, time_difference=time_difference ) @@ -91,6 +103,56 @@ def filter(self, raw_signal: SensorSignal) -> SensorSignal: velocity_rad_per_sec=velocity_out_rad_per_sec, ) + def __init_strategies__(self): + """Get instance of different options of drift removal, filtering, and velocity estimation.""" + if self._basic_config.drift_removal_method == DriftRemovalMethod.LOW_PASS: + self._drift_removal = LowPassDriftRemoval( + PreprocessorConfig.drift_removal_second_order_lpf_config + ) + + elif self._basic_config.drift_removal_method == DriftRemovalMethod.NOTCH: + self._drift_removal = NotchDriftRemoval( + PreprocessorConfig.drift_removal_notch_config + ) + else: + logger.warning("Selected method does not exist.") + + if self._basic_config.filtering_method == FilteringMethod.SOGI: + self._filtering = SogiFllFiltering( + PreprocessorConfig.filtering_sogifll_config + ) + + elif self._basic_config.filtering_method == FilteringMethod.LOW_PASS: + self._filtering = LowPassFiltering( + PreprocessorConfig.filtering_lowpass_config + ) + + else: + logger.warning("Selected method does not exist.") + + if ( + self._basic_config.velocity_estimation_method + == VelocityEstimationMethod.DISCRETE_DERIVATIVE + ): + self._velocity_estimation = DiscreteDerivativeVelocityEstimation() + + elif ( + self._basic_config.velocity_estimation_method + == VelocityEstimationMethod.LOW_PASS + ): + self._velocity_estimation = LowPassVelocityEstimation( + PreprocessorConfig.velocity_estimation_low_pass_config + ) + + elif ( + self._basic_config.velocity_estimation_method + == VelocityEstimationMethod.GYROSCOPE + ): + self._velocity_estimation = GyroscopeVelocityEstimation() + + else: + logger.warning("Selected method does not exist.") + def reset(self) -> None: """Reset the Signal Preprocessor if exosuit is disconnected or timeout occured. @@ -99,5 +161,5 @@ def reset(self) -> None: self._prev_timestamp = None self._drift_removal.reset() - self._sogi_fll.reset() + self._filtering.reset() self._velocity_estimation.reset() diff --git a/src/hip_controller/definitions.py b/src/hip_controller/definitions.py index 8fd05d5..c9f7057 100644 --- a/src/hip_controller/definitions.py +++ b/src/hip_controller/definitions.py @@ -1,14 +1,17 @@ """Common definitions for this module.""" -import sys +import sys from dataclasses import asdict, dataclass -from enum import auto + if sys.version_info >= (3, 11): - from enum import StrEnum + from enum import StrEnum, auto else: from enum import Enum + class StrEnum(str, Enum): """String enum backport for Python <3.11.""" + + from math import pi from pathlib import Path @@ -29,27 +32,27 @@ class StrEnum(str, Enum): LOG_DIR: Path = DATA_DIR / "logs" -@dataclass(frozen=True) -class BasicConfig: - """Basic configurations for the hip controller.""" +class DriftRemovalMethod(StrEnum): + """Drift removal strategy options.""" - # if the graph is displayed or not - left_limb_plot: bool = True - right_limb_plot: bool = True + LOW_PASS = auto() + NOTCH = auto() - # if the wiring settings are reversed or not - left_limb_reverse: bool = False - right_limb_reverse: bool = True - # either read data from imu or read data from csv file using csv player - read_from_imu: bool = False +class FilteringMethod(StrEnum): + """Filtering strategy options.""" - # the path where data is read from - read_data_from_path: Path = ( - DATA_DIR / "sensor_data" / "data_input_filtered_2026_01_09.csv" - ) + SOGI = auto() + KALMAN = auto() + LOW_PASS = auto() - frequency: int = 100 + +class VelocityEstimationMethod(StrEnum): + """Velocity estimation strategy options.""" + + DISCRETE_DERIVATIVE = auto() + LOW_PASS = auto() + GYROSCOPE = auto() class SolverType(StrEnum): @@ -65,17 +68,56 @@ class SolverType(StrEnum): Maps to Simulink continuous integrator + ode4 solver. """ - FORWARD_EULER = "forward_euler" - BACKWARD_EULER = "backward_euler" - TRAPEZOIDAL = "trapezoidal" - RUNGE_KUTTA = "rk4" + FORWARD_EULER = auto() + BACKWARD_EULER = auto() + TRAPEZOIDAL = auto() + RUNGE_KUTTA = auto() + + +@dataclass(frozen=True) +class BasicConfig: + """Basic configurations for the hip controller.""" + + # general frequency + frequency: int = 100 + + # if data is pre filtered - skip the pre processing + filtered: bool = False + + # if the graph is displayed or not + left_limb_plot: bool = False + right_limb_plot: bool = False + + # if the wiring settings are reversed or not + left_limb_reverse: bool = False + right_limb_reverse: bool = True + + # either read data from imu or read data from csv file using csv player + read_from_imu: bool = False + + # the path where data is read from + read_data_from_path: Path = ( + DATA_DIR / "sensor_data" / "data_input_filtered_2026_01_09.csv" + ) + + # select which DriftRemovalMethod, VelocityEstimationMethod + drift_removal_method: DriftRemovalMethod = DriftRemovalMethod.LOW_PASS + + filtering_method: FilteringMethod = FilteringMethod.SOGI + velocity_estimation_method: VelocityEstimationMethod = ( + VelocityEstimationMethod.DISCRETE_DERIVATIVE + ) + # cut-off frequency for the 2ndOrderLP filter + cut_off_freq_low_pass_rad_per_sec: float = 80.0 @dataclass class LowPassFilterConfig: """Settings for the second-order low-pass filter containing cut_off_frequency, damping_ratio, initial_condition, solver_type.""" - cut_off_frequency_rad_per_sec: float = 20.0 # in rad/s + cut_off_frequency_rad_per_sec: float = ( + BasicConfig.cut_off_freq_low_pass_rad_per_sec + ) # in rad/s damping_ratio: float = 1.0 # 1.0 = critically damped initial_condition: float = 0.0 solver_type: SolverType = ( @@ -90,8 +132,8 @@ class LowPassFilterConfig: class NotchConfig: """Configurations for the notch function.""" - center_freq_hz: float - bandwidth_3db_hz: float + center_freq_hz: float = 0.0 + bandwidth_3db_hz: float = 0.1 sample_rate_hz: float = BasicConfig.frequency @@ -155,74 +197,25 @@ class SogiFllConfig: numerical_safety_floor: float = 1e-9 -class DriftRemovalMethod(StrEnum): - """Drift removal strategy options.""" - - LOW_PASS = auto() - NOTCH = auto() - - -class VelocityEstimationMethod(StrEnum): - """Velocity estimation strategy options.""" - - SOGI = auto() - DISCRETE_DERIVATIVE = auto() - LOW_PASS = auto() - GYROSCOPE = auto() - - class PreprocessorConfig: """Configurations for the sensor preprocessor.""" - # Select methods for drift removal and velocity estimation filtering - drift_removal_method: DriftRemovalMethod = DriftRemovalMethod.LOW_PASS - velocity_estimation_method: VelocityEstimationMethod = ( - VelocityEstimationMethod.DISCRETE_DERIVATIVE - ) - # Configurations for the filters drift_removal_second_order_lpf_config: LowPassFilterConfig = LowPassFilterConfig( cut_off_frequency_rad_per_sec=1.25, damping_ratio=1.0, initial_condition=0.0 ) - drift_removal_notch_config: NotchConfig = NotchConfig( - center_freq_hz=0.0, bandwidth_3db_hz=0.1, sample_rate_hz=BasicConfig.frequency - ) + drift_removal_notch_config: NotchConfig = NotchConfig() + filtering_sogifll_config: SogiFllConfig = SogiFllConfig() + filtering_lowpass_config: LowPassFilterConfig = LowPassFilterConfig() + filtering_second_order_lpf_config: LowPassFilterConfig = LowPassFilterConfig( - cut_off_frequency_rad_per_sec=20.0, damping_ratio=1.0, initial_condition=0.0 + cut_off_frequency_rad_per_sec=80.0, damping_ratio=1.0, initial_condition=0.0 ) - @property - def drift_removal_strategy(self): - """Get instance of different options of drift removal.""" - from hip_controller.control.signal_processing.drift_removal import ( - LowPassDriftRemoval, - NotchDriftRemoval, - ) - - if self.drift_removal_method == DriftRemovalMethod.LOW_PASS: - return LowPassDriftRemoval(self.drift_removal_second_order_lpf_config) - else: - return NotchDriftRemoval(self.drift_removal_notch_config) - - @property - def velocity_estimation_strategy(self): - """Get instance of different options of velocity estimation.""" - from hip_controller.control.signal_processing.velocity_estimation import ( - DiscreteDerivativeVelocityEstimation, - GyroscopeVelocityEstimation, - LowPassVelocityEstimation, - ) - - if ( - self.velocity_estimation_method - == VelocityEstimationMethod.DISCRETE_DERIVATIVE - ): - return DiscreteDerivativeVelocityEstimation() - elif self.velocity_estimation_method == VelocityEstimationMethod.LOW_PASS: - return LowPassVelocityEstimation(self.filtering_second_order_lpf_config) - else: - return GyroscopeVelocityEstimation() + velocity_estimation_low_pass_config: LowPassFilterConfig = LowPassFilterConfig( + cut_off_frequency_rad_per_sec=20.0, damping_ratio=1.0, initial_condition=0.0 + ) # centering & normalization diff --git a/tests/conftest.py b/tests/conftest.py index c54532a..c5b9478 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,17 @@ import os import sys -from hip_controller.definitions import StrEnum from pathlib import Path +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from enum import Enum + + class StrEnum(str, Enum): + """String enum backport for Python <3.11.""" + + from hip_controller.definitions import TESTING_DIR # Add the src directory to the path so that the quaternion_ekf package can be imported diff --git a/tests/controller_test/app_test.py b/tests/controller_test/app_test.py index 90d6e3c..132e496 100644 --- a/tests/controller_test/app_test.py +++ b/tests/controller_test/app_test.py @@ -9,6 +9,7 @@ SensorSignal, WalkOnController, ) +from hip_controller.definitions import BasicConfig from tests.conftest import ( DATA_REFERENCE_MOTION_RIGHT, REL_TOL, @@ -18,7 +19,8 @@ def test_controller_right(): """Test the main function with the right lower limb data.""" - controller = WalkOnController(reverse=True, plot=False, filtered=True) + config = BasicConfig(filtered=True) + controller = WalkOnController(left_limb=False, config=config) df = read_csv(filepath_or_buffer=DATA_REFERENCE_MOTION_RIGHT) diff --git a/uv.lock b/uv.lock index 75fd7f4..3823cf6 100644 --- a/uv.lock +++ b/uv.lock @@ -441,7 +441,7 @@ wheels = [ [[package]] name = "hip-controller" -version = "0.1.1" +version = "0.1.2" source = { editable = "." } dependencies = [ { name = "loguru" },