Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
run: make init

- name: Run Pytest
run: make test
run: uv run pytest --cov=src --cov-report=term-missing --no-cov-on-fail --cov-report=xml --cov-fail-under=80 --ignore=tests/utils_test/csv_inspector_test.py --ignore=tests/utils_test/csv_player_test.py

- name: Upload coverage XML artifact
if: ${{ matrix.python-version == '3.12' }}
Expand Down
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
29 changes: 13 additions & 16 deletions scripts/controller_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,20 @@
from matplotlib import ticker
from pyqtgraph import QtCore, QtWidgets # pragma: no cover


from hip_controller.filters.second_order_low_pass_filter import (
SecondOrderLowPassFilter,
)
from hip_controller.control.app import WalkOnController
from hip_controller.definitions import (
DEFAULT_LOG_LEVEL,
BasicConfig,
LowPassFilterConfig,
SolverType,
ExosuitData
)

from src.hip_controller.plotter.csv_player import CSVPlayer
from dataclasses import dataclass
from scripts.csv_player import ScriptPlayer, ComparisonData
from hip_controller.control.app import WalkOnController
from scripts.live_comparison_plot import TimePlotterComparisonWindow
from hip_controller.filters.second_order_low_pass_filter import (
SecondOrderLowPassFilter,
)
from hip_controller.utils.utils import setup_logger

from scripts.csv_player import ComparisonData, ScriptPlayer
from scripts.live_comparison_plot import TimePlotterComparisonWindow
from src.hip_controller.plotter.csv_player import CSVPlayer


def simulate_comparison_dynamic(
Expand Down Expand Up @@ -125,9 +120,9 @@ def update() -> None:



sensor_data : ExosuitData = player.get_sensor_data_from_csv()
controller_left.step(sensor_data.left)
controller_right.step(sensor_data.right)
step = player.get_sensor_data_from_csv()
controller_left.step(step.sensor_data.left)
controller_right.step(step.sensor_data.right)

# setInterval in miliseconds. Update each 10ms
timer.setInterval(10)
Expand Down Expand Up @@ -275,7 +270,9 @@ def plot_notch_filter_debug(
if __name__ == "__main__":
simulate(input_name="x", expected_output_name="y", func=lpf.step, path=TESTING_DIR / "controller_test/low_level_testing/low_level_testing_data/second_order_lpf_2026_03_06.csv")
"""
from hip_controller.control.signal_processing.sensor_preprocessor import SensorPreprocessor
from hip_controller.control.signal_processing.sensor_preprocessor import (
SensorPreprocessor,
)
from hip_controller.definitions import PreprocessorConfig
preprocessor = SensorPreprocessor(PreprocessorConfig())

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')
```
15 changes: 10 additions & 5 deletions src/hip_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
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

try:
__version__ = version("hip-controller")
except PackageNotFoundError:
# this path leads to: src/hip_controller/__init__.py → src/ → repo_root/ → pyproject.toml
pyproject = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
with open(pyproject, "rb") as f:
__version__ = dict(tomllib.load(f))["project"]["version"]
try:
# this path leads to: src/hip_controller/__init__.py → src/ → repo_root/ → pyproject.toml
pyproject = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
with open(pyproject, "rb") as f:
__version__ = dict(tomllib.load(f))["project"]["version"]
except FileNotFoundError:
__version__ = "0.0.0+unknown"
Loading
Loading