Skip to content
Open
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
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<new.version.number>
Push the tag git push --tag

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -314,6 +304,9 @@ motor_command = limb_controller.step(signal)
The following tree shows the important permanent files. Run `make tree` to update.
<!-- TREE-START -->
```
├── .claude
│ └── skills
│ └── code-review-nathalie.md
├── data
│ ├── evaluation_raw_data
│ │ ├── incline_walk
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
124 changes: 124 additions & 0 deletions scripts/readme.md
Original file line number Diff line number Diff line change
@@ -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')
```
4 changes: 3 additions & 1 deletion src/hip_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions src/hip_controller/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 20 additions & 11 deletions src/hip_controller/control/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading
Loading