From 24fec5c3af61404d9a0c9544b63648e0580b00a2 Mon Sep 17 00:00:00 2001 From: zg1in Date: Wed, 20 May 2026 16:11:40 +0200 Subject: [PATCH 1/5] format --- src/hip_controller/__init__.py | 4 +++- src/hip_controller/definitions.py | 6 +++++- tests/conftest.py | 3 +-- 3 files changed, 9 insertions(+), 4 deletions(-) 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/definitions.py b/src/hip_controller/definitions.py index 8fd05d5..f172a3d 100644 --- a/src/hip_controller/definitions.py +++ b/src/hip_controller/definitions.py @@ -1,14 +1,18 @@ """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 else: from enum import Enum + class StrEnum(str, Enum): """String enum backport for Python <3.11.""" + + from math import pi from pathlib import Path diff --git a/tests/conftest.py b/tests/conftest.py index c54532a..736e420 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,10 +2,9 @@ import os import sys -from hip_controller.definitions import StrEnum from pathlib import Path -from hip_controller.definitions import TESTING_DIR +from hip_controller.definitions import TESTING_DIR, StrEnum # Add the src directory to the path so that the quaternion_ekf package can be imported my_path = os.path.dirname(os.path.abspath(__file__)) From 3d4d9ab74f70709dab096ae48af7b073b37428df Mon Sep 17 00:00:00 2001 From: zg1in Date: Wed, 20 May 2026 21:32:56 +0200 Subject: [PATCH 2/5] readme and config Co-authored-by: Copilot --- scripts/readme.md | 124 ++++++++++++++ src/hip_controller/__main__.py | 6 +- src/hip_controller/control/app.py | 26 ++- .../signal_processing/drift_removal.py | 4 +- .../signal_processing/sensor_preprocessor.py | 92 +++++++++-- src/hip_controller/definitions.py | 153 ++++++++---------- tests/conftest.py | 11 +- tests/controller_test/app_test.py | 4 +- 8 files changed, 308 insertions(+), 112 deletions(-) create mode 100644 scripts/readme.md 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/__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..74aae61 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,7 +19,7 @@ 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). @@ -28,20 +28,30 @@ def __init__(self, reverse: bool, plot: bool = False, filtered=False): :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 f172a3d..c9f7057 100644 --- a/src/hip_controller/definitions.py +++ b/src/hip_controller/definitions.py @@ -2,10 +2,9 @@ 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 @@ -33,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): @@ -69,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 = ( @@ -94,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 @@ -159,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 736e420..c5b9478 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,16 @@ import sys from pathlib import Path -from hip_controller.definitions import TESTING_DIR, StrEnum +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 my_path = os.path.dirname(os.path.abspath(__file__)) 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) From d5ff73ba9cee813897403a1f154d8c9e5b365d73 Mon Sep 17 00:00:00 2001 From: zg1in Date: Wed, 20 May 2026 21:34:57 +0200 Subject: [PATCH 3/5] version number --- README.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a25c35a..61a3b05 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 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 = [ From 0c78f172e290cd74883136b352735e00386e276d Mon Sep 17 00:00:00 2001 From: zg1in Date: Wed, 20 May 2026 21:37:02 +0200 Subject: [PATCH 4/5] new tree --- README.md | 17 +++++++++++------ uv.lock | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 61a3b05..97a4155 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,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 +796,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 +811,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 +836,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 +851,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 +896,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/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" }, From a3db610367017f41ab8f6fc92b9494f6f7bc2d09 Mon Sep 17 00:00:00 2001 From: zg1in Date: Wed, 20 May 2026 21:46:11 +0200 Subject: [PATCH 5/5] docstring and readme --- README.md | 20 +++++--------------- src/hip_controller/control/app.py | 5 ++--- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 97a4155..82fcf13 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/hip_controller/control/app.py b/src/hip_controller/control/app.py index 74aae61..43d4c6a 100644 --- a/src/hip_controller/control/app.py +++ b/src/hip_controller/control/app.py @@ -22,9 +22,8 @@ class WalkOnController: 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 """