diff --git a/.codex-worktrees/friction-factors-3659 b/.codex-worktrees/friction-factors-3659
new file mode 160000
index 0000000000..9c673194ef
--- /dev/null
+++ b/.codex-worktrees/friction-factors-3659
@@ -0,0 +1 @@
+Subproject commit 9c673194ef4c9a55595c3799d4fddd0d7e28c561
diff --git a/.codex-worktrees/pr-3602-fix b/.codex-worktrees/pr-3602-fix
new file mode 160000
index 0000000000..e37b3241d3
--- /dev/null
+++ b/.codex-worktrees/pr-3602-fix
@@ -0,0 +1 @@
+Subproject commit e37b3241d36d8841b6aa4c7688788fc5841aca48
diff --git a/.codex-worktrees/pr-3752-movement b/.codex-worktrees/pr-3752-movement
new file mode 160000
index 0000000000..e5e013c029
--- /dev/null
+++ b/.codex-worktrees/pr-3752-movement
@@ -0,0 +1 @@
+Subproject commit e5e013c02975432b4d15b16b9ce1f2b4938d5096
diff --git a/.codex-worktrees/pr-3766-modern-robotics-dbc b/.codex-worktrees/pr-3766-modern-robotics-dbc
new file mode 160000
index 0000000000..34ee67dce3
--- /dev/null
+++ b/.codex-worktrees/pr-3766-modern-robotics-dbc
@@ -0,0 +1 @@
+Subproject commit 34ee67dce3267f4ecae6eecb28e0288df80203bf
diff --git a/.codex-worktrees/pr-3780-pressure-flow b/.codex-worktrees/pr-3780-pressure-flow
new file mode 160000
index 0000000000..b286577f46
--- /dev/null
+++ b/.codex-worktrees/pr-3780-pressure-flow
@@ -0,0 +1 @@
+Subproject commit b286577f46dc8960f19b102f17aff100afc6977d
diff --git a/.codex-worktrees/pr-3784-deterministic-te b/.codex-worktrees/pr-3784-deterministic-te
new file mode 160000
index 0000000000..1e87cc7d5f
--- /dev/null
+++ b/.codex-worktrees/pr-3784-deterministic-te
@@ -0,0 +1 @@
+Subproject commit 1e87cc7d5fc99f3dde8893503f4e55d7f1df76b5
diff --git a/.jules/bolt.md b/.jules/bolt.md
index 6858e01b49..a02124c42a 100644
--- a/.jules/bolt.md
+++ b/.jules/bolt.md
@@ -89,3 +89,6 @@
## 2025-05-18 - Avoid array methods for small static arrays in frequently called initializers
**Learning:** Using `.reduce()` or `.map()` on static arrays like tabs definitions inside frequently called functions (e.g. state initializers or local storage hydration) incurs unnecessary closure and function call overhead.
**Action:** Replace `.reduce()` and `.map()` with single-pass `for` loops in simple data transformation functions (like `defaultTabVisibility`) to eliminate closure allocations.
+## 2026-08-09 - Pre-allocate Arrays instead of Array.from in hot paths
+**Learning:** In JavaScript/TypeScript data-intensive hot paths (e.g., CSV parsing), using `Array.from({ length: X }, ...)` for initialization incurs significant overhead from iterability checks, iterator creation, and closure execution per element.
+**Action:** Always replace `Array.from({ length: X }, ...)` with pre-allocated arrays using `new Array(X)` and populate them with standard `for` loops in performance-critical code to eliminate GC pressure.
diff --git a/SPEC.md b/SPEC.md
index eb9356b211..4d2e6e8ae7 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -2702,3 +2702,9 @@ The command injection check logic in `cli_tools.py` has been fortified. The inpu
- Removed chained array maps and reduces in the parseVariableAssignments function within `src/web_applications/calculator/static/app.js`.
- Improved execution speed by using standard single pass for loop and string `indexOf` / `substring` techniques.
+
+## Changelog
+- **2026-08-09**: Optimized CSV parsing in `p1am_control_system/frontend` and `data_explorer` by replacing `Array.from()` with standard `for` loops and pre-allocated arrays to eliminate iterability overhead in hot paths.
+## Changelog
+- **2026-08-09**: Optimized CSV parsing in `p1am_control_system/frontend` and `data_explorer` by replacing `Array.from()` with standard `for` loops and pre-allocated arrays to eliminate iterability overhead in hot paths.
+- **2026-08-09**: Optimized CSV parsing in `p1am_control_system/frontend` and `data_explorer` by replacing `Array.from()` with standard `for` loops and pre-allocated arrays to eliminate iterability overhead in hot paths.
diff --git a/launch.py b/launch.py
index ba04bb644b..4c4357053f 100644
--- a/launch.py
+++ b/launch.py
@@ -146,7 +146,9 @@ def launch_tool(tool_identifier: str) -> int:
gui_configs = registration.gui_configs
config = gui_configs.get(GUIType.PYQT6)
if config is None:
- print(f"Tool '{registration.display_name}' has no PyQt6 configuration.") # noqa: T201
+ print(
+ f"Tool '{registration.display_name}' has no PyQt6 configuration."
+ ) # noqa: T201
return 1
display_name = registration.display_name
diff --git a/scripts/bump_vendor_pin.py b/scripts/bump_vendor_pin.py
index 46471e6838..4363f2c4d7 100644
--- a/scripts/bump_vendor_pin.py
+++ b/scripts/bump_vendor_pin.py
@@ -95,9 +95,9 @@ def validate_consumer(consumer_repo: str) -> None:
Precondition: consumer_repo is a non-empty string.
Postcondition: no exception means the repo is safe to target.
"""
- assert isinstance(consumer_repo, str) and consumer_repo, (
- "consumer_repo must be a non-empty string"
- )
+ assert (
+ isinstance(consumer_repo, str) and consumer_repo
+ ), "consumer_repo must be a non-empty string"
if consumer_repo not in CONSUMER_REPOS:
raise ValueError(
f"Unknown consumer repo {consumer_repo!r}. Allowed: {CONSUMER_REPOS}"
diff --git a/scripts/runner_capacity_check.py b/scripts/runner_capacity_check.py
index 182cd9fffa..5bf6f6ac5b 100644
--- a/scripts/runner_capacity_check.py
+++ b/scripts/runner_capacity_check.py
@@ -248,18 +248,18 @@ def calculate_needed_runners(
Returns:
:class:`CapacityRecommendation` with suggested runner count.
"""
- assert isinstance(queue_depth, int) and queue_depth >= 0, (
- f"queue_depth must be a non-negative int, got {queue_depth!r}"
- )
- assert isinstance(current_runners, int) and current_runners > 0, (
- f"current_runners must be a positive int, got {current_runners!r}"
- )
- assert isinstance(target_wait_sec, int) and target_wait_sec > 0, (
- f"target_wait_sec must be a positive int, got {target_wait_sec!r}"
- )
- assert isinstance(avg_job_sec, int) and avg_job_sec > 0, (
- f"avg_job_sec must be a positive int, got {avg_job_sec!r}"
- )
+ assert (
+ isinstance(queue_depth, int) and queue_depth >= 0
+ ), f"queue_depth must be a non-negative int, got {queue_depth!r}"
+ assert (
+ isinstance(current_runners, int) and current_runners > 0
+ ), f"current_runners must be a positive int, got {current_runners!r}"
+ assert (
+ isinstance(target_wait_sec, int) and target_wait_sec > 0
+ ), f"target_wait_sec must be a positive int, got {target_wait_sec!r}"
+ assert (
+ isinstance(avg_job_sec, int) and avg_job_sec > 0
+ ), f"avg_job_sec must be a positive int, got {avg_job_sec!r}"
if queue_depth == 0:
return CapacityRecommendation(
@@ -341,16 +341,16 @@ def check_and_alert(
Advisory string: one of ``"OK"``, ``"WARN: ..."``, or ``"ALERT: ..."``.
"""
assert isinstance(token, str) and token, "token must be a non-empty string"
- assert isinstance(current_runners, int) and current_runners > 0, (
- f"current_runners must be a positive int, got {current_runners!r}"
- )
+ assert (
+ isinstance(current_runners, int) and current_runners > 0
+ ), f"current_runners must be a positive int, got {current_runners!r}"
assert isinstance(org, str) and org, "org must be a non-empty string"
- assert isinstance(alert_threshold, int) and alert_threshold > 0, (
- f"alert_threshold must be a positive int, got {alert_threshold!r}"
- )
- assert isinstance(target_wait_sec, int) and target_wait_sec > 0, (
- f"target_wait_sec must be a positive int, got {target_wait_sec!r}"
- )
+ assert (
+ isinstance(alert_threshold, int) and alert_threshold > 0
+ ), f"alert_threshold must be a positive int, got {alert_threshold!r}"
+ assert (
+ isinstance(target_wait_sec, int) and target_wait_sec > 0
+ ), f"target_wait_sec must be a positive int, got {target_wait_sec!r}"
queue_depth = get_queue_depth(token=token, org=org)
rec = calculate_needed_runners(
diff --git a/src/data_processing/data_processor/python/benchmarks/performance_benchmark.py b/src/data_processing/data_processor/python/benchmarks/performance_benchmark.py
index 9bf1145422..12d4fb4b30 100644
--- a/src/data_processing/data_processor/python/benchmarks/performance_benchmark.py
+++ b/src/data_processing/data_processor/python/benchmarks/performance_benchmark.py
@@ -150,9 +150,9 @@ def benchmark_file_loading(self) -> dict[str, dict[str, float | int]]:
elapsed = time.perf_counter() - start
# Validate all files loaded successfully
- assert len(dataframes) == len(files), (
- f"Expected {len(files)} dataframes, got {len(dataframes)}"
- )
+ assert len(dataframes) == len(
+ files
+ ), f"Expected {len(files)} dataframes, got {len(dataframes)}"
results["load_multiple_5_files"] = {
"time": elapsed,
@@ -225,9 +225,9 @@ def benchmark_filtering(self) -> dict[str, dict[str, float]]:
elapsed = time.perf_counter() - start
# Validate filter output
- assert filtered_df is not None and len(filtered_df) == n_rows, (
- f"Filter {filter_name} failed"
- )
+ assert (
+ filtered_df is not None and len(filtered_df) == n_rows
+ ), f"Filter {filter_name} failed"
throughput = n_rows / elapsed
results[f"filter_{filter_name}"] = {
@@ -384,9 +384,9 @@ def benchmark_end_to_end_workflow(self) -> dict[str, dict[str, float]]:
stats_time = time.perf_counter() - start
# Validate statistics output
- assert stats is not None and "mean" in stats, (
- "Statistics calculation failed"
- )
+ assert (
+ stats is not None and "mean" in stats
+ ), "Statistics calculation failed"
# Step 6: Save
start = time.perf_counter()
@@ -437,9 +437,9 @@ def benchmark_scalability(self) -> dict[str, dict[str, float]]:
elapsed = time.perf_counter() - start
# Validate filter output
- assert filtered is not None and len(filtered) == n_rows, (
- f"Scalability test failed for {n_rows} rows"
- )
+ assert (
+ filtered is not None and len(filtered) == n_rows
+ ), f"Scalability test failed for {n_rows} rows"
throughput = n_rows / elapsed
@@ -474,9 +474,9 @@ def benchmark_memory_usage(self) -> dict[str, dict[str, float]]:
filtered = self.processor.apply_filter(df, config)
# Validate filter was applied
- assert filtered is not None and len(filtered) == n_rows, (
- "Memory benchmark filter failed"
- )
+ assert (
+ filtered is not None and len(filtered) == n_rows
+ ), "Memory benchmark filter failed"
memory_after = self.get_memory_usage_mb()
diff --git a/src/data_processing/data_processor/python/data_processor/core/data_loader.py b/src/data_processing/data_processor/python/data_processor/core/data_loader.py
index 8cf2d2984e..3488e1c478 100644
--- a/src/data_processing/data_processor/python/data_processor/core/data_loader.py
+++ b/src/data_processing/data_processor/python/data_processor/core/data_loader.py
@@ -124,7 +124,9 @@ def _create_high_performance_loader(self) -> HighPerformanceDataLoader | None:
try:
loader_class = self._import_high_performance_loader()
return loader_class()
- except Exception as exc: # noqa: BLE001 - optional accelerator, any failure degrades
+ except (
+ Exception
+ ) as exc: # noqa: BLE001 - optional accelerator, any failure degrades
logger.warning(
"High-performance loader unavailable; using standard loader: %s",
exc,
diff --git a/src/data_processing/data_processor/python/tests/test_nn_training_worker.py b/src/data_processing/data_processor/python/tests/test_nn_training_worker.py
index 863457b2d0..38e6634c14 100644
--- a/src/data_processing/data_processor/python/tests/test_nn_training_worker.py
+++ b/src/data_processing/data_processor/python/tests/test_nn_training_worker.py
@@ -65,9 +65,9 @@ def test_worker_runs_off_main_thread(qtbot: Any, sample_df: pd.DataFrame) -> Non
assert results == [{"ok": True, "rows": 100}]
assert trainer.train_thread is not None
- assert trainer.train_thread != main_thread_id, (
- "train() ran on the Qt main thread — UI would freeze"
- )
+ assert (
+ trainer.train_thread != main_thread_id
+ ), "train() ran on the Qt main thread — UI would freeze"
def test_worker_ui_stays_responsive(qtbot: Any, sample_df: pd.DataFrame) -> None:
diff --git a/src/data_processing/data_processor/python/tests/test_vectorized_filter_engine_contracts.py b/src/data_processing/data_processor/python/tests/test_vectorized_filter_engine_contracts.py
index 5ed61af3b6..1693775f77 100644
--- a/src/data_processing/data_processor/python/tests/test_vectorized_filter_engine_contracts.py
+++ b/src/data_processing/data_processor/python/tests/test_vectorized_filter_engine_contracts.py
@@ -142,9 +142,9 @@ def test_output_columns_preserved(
) -> None:
df = _make_df(n=300)
result = engine.apply_filter_batch(df, filter_type, params)
- assert list(result.columns) == list(df.columns), (
- f"{filter_type}: columns changed"
- )
+ assert list(result.columns) == list(
+ df.columns
+ ), f"{filter_type}: columns changed"
@pytest.mark.parametrize("filter_type,params", FILTER_TYPES)
def test_output_row_count_preserved(
@@ -152,9 +152,9 @@ def test_output_row_count_preserved(
) -> None:
df = _make_df(n=300)
result = engine.apply_filter_batch(df, filter_type, params)
- assert len(result) == len(df), (
- f"{filter_type}: row count changed {len(result)} != {len(df)}"
- )
+ assert len(result) == len(
+ df
+ ), f"{filter_type}: row count changed {len(result)} != {len(df)}"
class TestMovingAverageCorrectness:
@@ -210,9 +210,9 @@ def test_nan_rows_remain_nan(self, engine, filter_type: str, params: dict) -> No
nan_after = result["x"].index[result["x"].isna()]
# All original NaN positions should still be NaN
for idx in nan_idx:
- assert idx in nan_after, (
- f"{filter_type}: NaN at index {idx} was filled unexpectedly"
- )
+ assert (
+ idx in nan_after
+ ), f"{filter_type}: NaN at index {idx} was filled unexpectedly"
class TestParallelVsSequentialConsistency:
diff --git a/src/flow_rate_converter/tests/test_flow_rate_converter_gui.py b/src/flow_rate_converter/tests/test_flow_rate_converter_gui.py
index 1febc03a6a..28435a282b 100644
--- a/src/flow_rate_converter/tests/test_flow_rate_converter_gui.py
+++ b/src/flow_rate_converter/tests/test_flow_rate_converter_gui.py
@@ -51,12 +51,12 @@ def test_lod_constants_present_in_source(self):
if isinstance(target, ast.Name):
top_level_names.add(target.id)
- assert "_ALIGN_CENTER" in top_level_names, (
- "Missing _ALIGN_CENTER constant in main_window"
- )
- assert "_EXPANDING" in top_level_names, (
- "Missing _EXPANDING constant in main_window"
- )
+ assert (
+ "_ALIGN_CENTER" in top_level_names
+ ), "Missing _ALIGN_CENTER constant in main_window"
+ assert (
+ "_EXPANDING" in top_level_names
+ ), "Missing _EXPANDING constant in main_window"
assert "_FIXED" in top_level_names, "Missing _FIXED constant in main_window"
def test_no_bare_qt_alignment_flag_chain_in_source(self):
diff --git a/src/humanoid_builder_gui/tests/test_humanoid_builder_gui.py b/src/humanoid_builder_gui/tests/test_humanoid_builder_gui.py
index 805673832a..d17216cbb6 100644
--- a/src/humanoid_builder_gui/tests/test_humanoid_builder_gui.py
+++ b/src/humanoid_builder_gui/tests/test_humanoid_builder_gui.py
@@ -326,8 +326,8 @@ def test_no_deep_attribute_chains_in_method_body(
)
matches = pattern.findall(source)
# Only the alias definitions should match (7 lines)
- assert len(matches) <= 7, (
- f"Unexpected deep attribute chains found: {matches}"
- )
+ assert (
+ len(matches) <= 7
+ ), f"Unexpected deep attribute chains found: {matches}"
except ImportError:
pytest.skip("PyQt6 not available in this environment")
diff --git a/src/lower_body_model/launch_pyqt6.py b/src/lower_body_model/launch_pyqt6.py
index e38b99ee2a..630990a0bb 100644
--- a/src/lower_body_model/launch_pyqt6.py
+++ b/src/lower_body_model/launch_pyqt6.py
@@ -351,7 +351,9 @@ def on_torque_imported(self, joint_name: str, coeffs: object) -> None:
c = [float(x) for x in coeffs]
self.sim.set_joint_polynomial(joint_name, c)
logging.info(f"Imported torque polynomial for {joint_name}: {c}")
- except Exception as e: # noqa: BLE001 — caller-supplied data may be any type
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — caller-supplied data may be any type
logging.error(f"Failed to set polynomial: {e}")
def physics_loop(self) -> None:
diff --git a/src/media_processing/video_processor/python/video_processor_src/constants.py b/src/media_processing/video_processor/python/video_processor_src/constants.py
index 667e17c350..7f1f5b2a99 100644
--- a/src/media_processing/video_processor/python/video_processor_src/constants.py
+++ b/src/media_processing/video_processor/python/video_processor_src/constants.py
@@ -14,7 +14,9 @@
# Mathematical constants
PI: float = math.pi # [dimensionless] Ratio of circumference to diameter
-E: float = 2.718281828459045 # [dimensionless] Euler's number, base of natural logarithm # noqa: E501
+E: float = (
+ 2.718281828459045 # [dimensionless] Euler's number, base of natural logarithm # noqa: E501
+)
# Physical constants - SI units
GRAVITY_M_S2: float = 9.80665 # [m/s²] Standard gravity, ISO 80000-3:2006
diff --git a/src/movement_optimizer/cli.py b/src/movement_optimizer/cli.py
index 221326cd38..61b3e2ced4 100644
--- a/src/movement_optimizer/cli.py
+++ b/src/movement_optimizer/cli.py
@@ -56,13 +56,17 @@ def _add_body_args(parser: argparse.ArgumentParser) -> None:
"--body-mass",
type=float,
default=75.0,
- help=(f"Body mass in kg (range {BODY_MASS_RANGE[0]}-{BODY_MASS_RANGE[1]}, default: 75.0)."),
+ help=(
+ f"Body mass in kg (range {BODY_MASS_RANGE[0]}-{BODY_MASS_RANGE[1]}, default: 75.0)."
+ ),
)
parser.add_argument(
"--height",
type=float,
default=1.75,
- help=(f"Height in metres (range {HEIGHT_RANGE[0]}-{HEIGHT_RANGE[1]}, default: 1.75)."),
+ help=(
+ f"Height in metres (range {HEIGHT_RANGE[0]}-{HEIGHT_RANGE[1]}, default: 1.75)."
+ ),
)
parser.add_argument(
"--bar-mass",
@@ -99,7 +103,9 @@ def _add_run_args(parser: argparse.ArgumentParser) -> None:
default=None,
help="Path to save results as JSON. If omitted, prints summary to stdout.",
)
- parser.add_argument("--verbose", action="store_true", help="Enable verbose logging.")
+ parser.add_argument(
+ "--verbose", action="store_true", help="Enable verbose logging."
+ )
def _build_parser() -> argparse.ArgumentParser:
@@ -263,7 +269,9 @@ def _build_optimizer(
return opt, dyn
-def _save_or_emit(result: OptimizationResult, exercise: str, output: str | None) -> None:
+def _save_or_emit(
+ result: OptimizationResult, exercise: str, output: str | None
+) -> None:
"""Write result to file or emit summary to stdout.
Args:
@@ -279,7 +287,9 @@ def _save_or_emit(result: OptimizationResult, exercise: str, output: str | None)
_emit_cli_summary(_result_to_summary(result, exercise))
-def _validate_cli_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
+def _validate_cli_args(
+ parser: argparse.ArgumentParser, args: argparse.Namespace
+) -> None:
"""Reject invalid numeric CLI arguments via parser.error.
Delegates to :func:`movement_optimizer.validation.validate_all` so the
@@ -338,9 +348,13 @@ def main(argv: list[str] | None = None) -> int:
_configure_logging(args.verbose)
body = BodyModel(body_mass=args.body_mass, height=args.height)
duration = _resolve_duration(args.exercise, args.duration)
- _log_optimization_start(args.exercise, args.body_mass, args.height, args.bar_mass, duration)
+ _log_optimization_start(
+ args.exercise, args.body_mass, args.height, args.bar_mass, duration
+ )
t_start = time.perf_counter()
- opt, _dyn = _build_optimizer(body, args.exercise, args.bar_mass, duration, args.smoothness)
+ opt, _dyn = _build_optimizer(
+ body, args.exercise, args.bar_mass, duration, args.smoothness
+ )
result = opt.optimize()
_log_optimization_done(time.perf_counter() - t_start, result.cost, result.success)
_save_or_emit(result, args.exercise, args.output)
diff --git a/src/movement_optimizer/constants.py b/src/movement_optimizer/constants.py
index 197cab7f59..5d07b48a85 100644
--- a/src/movement_optimizer/constants.py
+++ b/src/movement_optimizer/constants.py
@@ -187,7 +187,9 @@
# ~7 mm for a 1.75 m person — effectively a grip-only link.
WRIST_SEGMENT_FRAC: float = 0.01
-BENCH_UPPER_ARM_FRAC: float = 0.56 # shoulder to elbow (anatomical ~48% + shoulder width)
+BENCH_UPPER_ARM_FRAC: float = (
+ 0.56 # shoulder to elbow (anatomical ~48% + shoulder width)
+)
BENCH_FOREARM_FRAC: float = 0.44 # elbow to wrist (Winter 2009: ~44% of arm length)
BENCH_PRESS_JOINT_LIMITS: dict[str, tuple[float, float]] = {
diff --git a/src/movement_optimizer/exercises/_common.py b/src/movement_optimizer/exercises/_common.py
index b31a03de79..d3be2357fc 100644
--- a/src/movement_optimizer/exercises/_common.py
+++ b/src/movement_optimizer/exercises/_common.py
@@ -24,7 +24,9 @@ def balance_config_pose(
adjust_joint: int,
) -> NDArray:
"""Balance a raw pose using the shared planar balance helper."""
- return balance_pose(dynamics, raw_pose, exercise_type, bar_mass, adjust_joint=adjust_joint)
+ return balance_pose(
+ dynamics, raw_pose, exercise_type, bar_mass, adjust_joint=adjust_joint
+ )
def default_bounds_deg(
diff --git a/src/movement_optimizer/exercises/clean.py b/src/movement_optimizer/exercises/clean.py
index b13caa62b0..9e5d8978df 100644
--- a/src/movement_optimizer/exercises/clean.py
+++ b/src/movement_optimizer/exercises/clean.py
@@ -58,7 +58,9 @@ def make_clean_config(
dyn = LagrangianDynamics(body, body.m_deadlift.copy(), body.I_deadlift.copy(), load)
q_start_raw = pull_start_angles(body, q2_deg=52)
- q_start = balance_config_pose(dyn, q_start_raw, "deadlift", bar_mass, adjust_joint=0)
+ q_start = balance_config_pose(
+ dyn, q_start_raw, "deadlift", bar_mass, adjust_joint=0
+ )
q_end_raw = _clean_end_angles(body)
q_end = balance_config_pose(dyn, q_end_raw, "deadlift", bar_mass, adjust_joint=2)
diff --git a/src/movement_optimizer/exercises/gait.py b/src/movement_optimizer/exercises/gait.py
index df7948fa04..8ac66561a7 100644
--- a/src/movement_optimizer/exercises/gait.py
+++ b/src/movement_optimizer/exercises/gait.py
@@ -147,7 +147,9 @@ def compute_spatiotemporal(
"cycle_duration_s": duration,
}
- def compute_symmetry_index(self, left_angles: NDArray, right_angles: NDArray) -> float:
+ def compute_symmetry_index(
+ self, left_angles: NDArray, right_angles: NDArray
+ ) -> float:
"""Robinson symmetry index: SI = |L-R| / max(L,R) * 100.
Preconditions:
diff --git a/src/movement_optimizer/exercises/sit_to_stand.py b/src/movement_optimizer/exercises/sit_to_stand.py
index 15ef0c440d..b5fc0f9cbe 100644
--- a/src/movement_optimizer/exercises/sit_to_stand.py
+++ b/src/movement_optimizer/exercises/sit_to_stand.py
@@ -23,7 +23,9 @@
logger = logging.getLogger(__name__)
-def _sts_via_points(q_start: NDArray, q_end: NDArray) -> list[tuple[float, float, float, float]]:
+def _sts_via_points(
+ q_start: NDArray, q_end: NDArray
+) -> list[tuple[float, float, float, float]]:
"""Via-points for sit-to-stand motion."""
return [
(0.00, float(q_start[0]), float(q_start[1]), float(q_start[2])), # seated
diff --git a/src/movement_optimizer/exercises/snatch.py b/src/movement_optimizer/exercises/snatch.py
index 0b1d533b85..4b6cf448fa 100644
--- a/src/movement_optimizer/exercises/snatch.py
+++ b/src/movement_optimizer/exercises/snatch.py
@@ -64,7 +64,9 @@ def make_snatch_config(
dyn = LagrangianDynamics(body, body.m_deadlift.copy(), body.I_deadlift.copy(), load)
q_start_raw = pull_start_angles(body, q2_deg=48)
- q_start = balance_config_pose(dyn, q_start_raw, "deadlift", bar_mass, adjust_joint=0)
+ q_start = balance_config_pose(
+ dyn, q_start_raw, "deadlift", bar_mass, adjust_joint=0
+ )
# End: standing with bar overhead -- use squat-style COM for balance check
# but keep the same dynamics object
diff --git a/src/movement_optimizer/export.py b/src/movement_optimizer/export.py
index 5708b383e9..861b295636 100644
--- a/src/movement_optimizer/export.py
+++ b/src/movement_optimizer/export.py
@@ -114,7 +114,9 @@ def export_animation_gif(
# matplotlib stubs type AbstractMovieWriter narrowly; PillowWriter is
# compatible at runtime.
anim.save(str(safe_path), writer=cast(Any, writer))
- logger.info("Exported GIF animation to %s (%d frames, %d fps)", safe_path, n_frames, fps)
+ logger.info(
+ "Exported GIF animation to %s (%d frames, %d fps)", safe_path, n_frames, fps
+ )
def export_plots_png(
diff --git a/src/movement_optimizer/export_excel.py b/src/movement_optimizer/export_excel.py
index 555269e5be..da24c2b8dd 100644
--- a/src/movement_optimizer/export_excel.py
+++ b/src/movement_optimizer/export_excel.py
@@ -67,7 +67,14 @@ def _write_summary_sheet(
ws.append([]) # blank separator
joint_labels = ["Ankle (joint 1)", "Knee (joint 2)", "Hip (joint 3)"]
- ws.append(["Joint torque statistics", "Peak |tau| (N*m)", "Mean |tau| (N*m)", "RMS tau (N*m)"])
+ ws.append(
+ [
+ "Joint torque statistics",
+ "Peak |tau| (N*m)",
+ "Mean |tau| (N*m)",
+ "RMS tau (N*m)",
+ ]
+ )
n_dof = result.torques.shape[1]
for j in range(n_dof):
col = result.torques[:, j]
diff --git a/src/movement_optimizer/gui/_sidebar_builders.py b/src/movement_optimizer/gui/_sidebar_builders.py
index 8a9ca251b1..fa11c5d916 100644
--- a/src/movement_optimizer/gui/_sidebar_builders.py
+++ b/src/movement_optimizer/gui/_sidebar_builders.py
@@ -220,7 +220,9 @@ def build_buttons(sidebar: ParameterSidebar) -> None:
sidebar.cancel_btn.setProperty("class", "cancel")
sidebar.cancel_btn.setToolTip("Cancel the currently running optimization (Esc)")
sidebar.cancel_btn.setAccessibleName("Cancel")
- sidebar.cancel_btn.setAccessibleDescription("Cancel the currently running optimization.")
+ sidebar.cancel_btn.setAccessibleDescription(
+ "Cancel the currently running optimization."
+ )
sidebar.cancel_btn.setShortcut("Esc")
sidebar.cancel_btn.clicked.connect(sidebar.cancel_requested.emit)
sidebar.cancel_btn.setVisible(False)
@@ -296,16 +298,22 @@ def build_results(sidebar: ParameterSidebar) -> None:
sidebar.export_btn = QPushButton(tr("Export") + " CSV")
sidebar.export_btn.setEnabled(False)
- sidebar.export_btn.setToolTip("Run optimization first to enable exporting kinematics to CSV")
+ sidebar.export_btn.setToolTip(
+ "Run optimization first to enable exporting kinematics to CSV"
+ )
sidebar.export_btn.setAccessibleName("Export CSV")
- sidebar.export_btn.setAccessibleDescription("Export optimized kinematics to a CSV file.")
+ sidebar.export_btn.setAccessibleDescription(
+ "Export optimized kinematics to a CSV file."
+ )
sidebar.export_btn.clicked.connect(sidebar.export_requested.emit)
sidebar.main_layout.addWidget(sidebar.export_btn)
sidebar.reset_btn = QPushButton("Reset Defaults")
sidebar.reset_btn.setToolTip("Reset all parameters to default values")
sidebar.reset_btn.setAccessibleName("Reset Defaults")
- sidebar.reset_btn.setAccessibleDescription("Reset all parameters to their default values.")
+ sidebar.reset_btn.setAccessibleDescription(
+ "Reset all parameters to their default values."
+ )
sidebar.reset_btn.clicked.connect(sidebar.reset_requested.emit)
sidebar.main_layout.addWidget(sidebar.reset_btn)
@@ -319,15 +327,21 @@ def build_persistence_buttons(sidebar: ParameterSidebar) -> None:
lay = QVBoxLayout(grp)
sidebar.save_btn = QPushButton("Save Solution")
sidebar.save_btn.setEnabled(False)
- sidebar.save_btn.setToolTip("Run optimization first to enable saving the trajectory solution")
+ sidebar.save_btn.setToolTip(
+ "Run optimization first to enable saving the trajectory solution"
+ )
sidebar.save_btn.setAccessibleName("Save Solution")
- sidebar.save_btn.setAccessibleDescription("Save the current trajectory solution to a file.")
+ sidebar.save_btn.setAccessibleDescription(
+ "Save the current trajectory solution to a file."
+ )
sidebar.save_btn.clicked.connect(sidebar.save_solution_requested.emit)
lay.addWidget(sidebar.save_btn)
sidebar.load_btn = QPushButton("Load Solution")
sidebar.load_btn.setToolTip("Load a previously saved trajectory solution file")
sidebar.load_btn.setAccessibleName("Load Solution")
- sidebar.load_btn.setAccessibleDescription("Load a previously saved trajectory solution file.")
+ sidebar.load_btn.setAccessibleDescription(
+ "Load a previously saved trajectory solution file."
+ )
sidebar.load_btn.clicked.connect(sidebar.load_solution_requested.emit)
lay.addWidget(sidebar.load_btn)
sidebar.main_layout.addWidget(grp)
@@ -338,16 +352,24 @@ def build_export_buttons(sidebar: ParameterSidebar) -> None:
lay = QVBoxLayout(grp)
sidebar.export_video_btn = QPushButton("Export Animation GIF")
sidebar.export_video_btn.setEnabled(False)
- sidebar.export_video_btn.setToolTip("Run optimization first to enable exporting animation GIF")
+ sidebar.export_video_btn.setToolTip(
+ "Run optimization first to enable exporting animation GIF"
+ )
sidebar.export_video_btn.setAccessibleName("Export Animation GIF")
- sidebar.export_video_btn.setAccessibleDescription("Export the optimized animation as a GIF.")
+ sidebar.export_video_btn.setAccessibleDescription(
+ "Export the optimized animation as a GIF."
+ )
sidebar.export_video_btn.clicked.connect(sidebar.export_video_requested.emit)
lay.addWidget(sidebar.export_video_btn)
sidebar.export_plots_btn = QPushButton("Export Plots (PNG/PDF)")
sidebar.export_plots_btn.setEnabled(False)
- sidebar.export_plots_btn.setToolTip("Run optimization first to enable exporting plots")
+ sidebar.export_plots_btn.setToolTip(
+ "Run optimization first to enable exporting plots"
+ )
sidebar.export_plots_btn.setAccessibleName("Export Plots")
- sidebar.export_plots_btn.setAccessibleDescription("Export analysis plots as PNG or PDF files.")
+ sidebar.export_plots_btn.setAccessibleDescription(
+ "Export analysis plots as PNG or PDF files."
+ )
sidebar.export_plots_btn.clicked.connect(sidebar.export_plots_requested.emit)
lay.addWidget(sidebar.export_plots_btn)
sidebar.export_excel_btn = QPushButton("Save as Excel (.xlsx)")
@@ -367,7 +389,9 @@ def build_comparison_buttons(sidebar: ParameterSidebar) -> None:
lay = QVBoxLayout(grp)
sidebar.add_compare_btn = QPushButton("Add to Comparison")
sidebar.add_compare_btn.setEnabled(False)
- sidebar.add_compare_btn.setToolTip("Run optimization first to add current trial to comparison")
+ sidebar.add_compare_btn.setToolTip(
+ "Run optimization first to add current trial to comparison"
+ )
sidebar.add_compare_btn.setAccessibleName("Add to Comparison")
sidebar.add_compare_btn.setAccessibleDescription(
"Add the current optimized trial to the comparison set."
@@ -382,9 +406,13 @@ def build_comparison_buttons(sidebar: ParameterSidebar) -> None:
sidebar.compare_btn.clicked.connect(sidebar.compare_trials_requested.emit)
lay.addWidget(sidebar.compare_btn)
sidebar.clear_compare_btn = QPushButton("Clear Comparison")
- sidebar.clear_compare_btn.setToolTip("Clear all trials currently saved for comparison")
+ sidebar.clear_compare_btn.setToolTip(
+ "Clear all trials currently saved for comparison"
+ )
sidebar.clear_compare_btn.setAccessibleName("Clear Comparison")
- sidebar.clear_compare_btn.setAccessibleDescription("Clear all trials from the comparison set.")
+ sidebar.clear_compare_btn.setAccessibleDescription(
+ "Clear all trials from the comparison set."
+ )
sidebar.clear_compare_btn.clicked.connect(sidebar.clear_comparison_requested.emit)
lay.addWidget(sidebar.clear_compare_btn)
sidebar.main_layout.addWidget(grp)
diff --git a/src/movement_optimizer/gui/_sidebar_state.py b/src/movement_optimizer/gui/_sidebar_state.py
index 5138ef0748..e2c2cee5f7 100644
--- a/src/movement_optimizer/gui/_sidebar_state.py
+++ b/src/movement_optimizer/gui/_sidebar_state.py
@@ -61,9 +61,13 @@ class SidebarStateContract(Protocol):
def show_optimizing(sidebar: SidebarStateContract) -> None:
sidebar.opt_btn.setEnabled(False)
- sidebar.opt_btn.setToolTip("Optimization currently in progress. Please wait or cancel.")
+ sidebar.opt_btn.setToolTip(
+ "Optimization currently in progress. Please wait or cancel."
+ )
sidebar.both_btn.setEnabled(False)
- sidebar.both_btn.setToolTip("Optimization currently in progress. Please wait or cancel.")
+ sidebar.both_btn.setToolTip(
+ "Optimization currently in progress. Please wait or cancel."
+ )
sidebar.cancel_btn.setVisible(True)
sidebar.cancel_btn.setToolTip("Cancel the currently running optimization (Esc)")
sidebar.stall_label.setVisible(False)
@@ -98,10 +102,16 @@ def update_progress(sidebar: SidebarStateContract, report: ProgressReport) -> No
phase = "Converging" if n_evals > PROGRESS_PHASE_BOUNDARY_EVALS else "Exploring"
sidebar.prog_label.setText(f"{phase}...")
sidebar.iter_label.setText(f"Evaluations: {report.iteration}")
- sidebar.cost_label.setText(f"Cost: {report.cost:.1f} (best: {report.best_cost:.1f})")
+ sidebar.cost_label.setText(
+ f"Cost: {report.cost:.1f} (best: {report.best_cost:.1f})"
+ )
sidebar.improve_label.setText(f"Improvement: {report.improvement_pct:+.3f}%")
elapsed = report.elapsed_s
- time_str = f"{elapsed:.1f}s" if elapsed < 60 else f"{int(elapsed // 60)}m {elapsed % 60:.0f}s"
+ time_str = (
+ f"{elapsed:.1f}s"
+ if elapsed < 60
+ else f"{int(elapsed // 60)}m {elapsed % 60:.0f}s"
+ )
sidebar.elapsed_label.setText(f"Elapsed: {time_str}")
if report.is_stalled:
diff --git a/src/movement_optimizer/gui/bilateral_3d_renderer.py b/src/movement_optimizer/gui/bilateral_3d_renderer.py
index 6a70a78a86..2811c1e741 100644
--- a/src/movement_optimizer/gui/bilateral_3d_renderer.py
+++ b/src/movement_optimizer/gui/bilateral_3d_renderer.py
@@ -65,7 +65,9 @@ def draw_bilateral_3d_pose(
# Ground plane hint: a thin disc at z=0.
theta = np.linspace(0.0, 2.0 * np.pi, 40)
r = max(0.6, 0.75 * (model.stance_width_m + 0.5))
- ax.plot(r * np.cos(theta), r * np.sin(theta), 0.0, color=Palette.FG_DIM, lw=1, alpha=0.3)
+ ax.plot(
+ r * np.cos(theta), r * np.sin(theta), 0.0, color=Palette.FG_DIM, lw=1, alpha=0.3
+ )
# Reasonable default view.
total_h = model.L_shin + model.L_thigh + model.L_torso
diff --git a/src/movement_optimizer/gui/commands.py b/src/movement_optimizer/gui/commands.py
index 9a194063be..c4229db080 100644
--- a/src/movement_optimizer/gui/commands.py
+++ b/src/movement_optimizer/gui/commands.py
@@ -63,7 +63,9 @@ def push(self, cmd: Command) -> None:
cmd.execute()
self._undo.append(cmd)
self._redo.clear()
- logger.debug("UndoStack: pushed %s (depth=%d)", type(cmd).__name__, len(self._undo))
+ logger.debug(
+ "UndoStack: pushed %s (depth=%d)", type(cmd).__name__, len(self._undo)
+ )
def record_executed(self, cmd: Command) -> None:
"""Record an already-applied command without calling ``execute``.
@@ -73,7 +75,9 @@ def record_executed(self, cmd: Command) -> None:
"""
self._undo.append(cmd)
self._redo.clear()
- logger.debug("UndoStack: recorded %s (depth=%d)", type(cmd).__name__, len(self._undo))
+ logger.debug(
+ "UndoStack: recorded %s (depth=%d)", type(cmd).__name__, len(self._undo)
+ )
def undo(self) -> bool:
"""Undo the most recently executed command.
@@ -87,7 +91,9 @@ def undo(self) -> bool:
cmd = self._undo.pop()
cmd.undo()
self._redo.append(cmd)
- logger.debug("UndoStack: undid %s (remaining=%d)", type(cmd).__name__, len(self._undo))
+ logger.debug(
+ "UndoStack: undid %s (remaining=%d)", type(cmd).__name__, len(self._undo)
+ )
return True
def redo(self) -> bool:
@@ -102,7 +108,9 @@ def redo(self) -> bool:
cmd = self._redo.pop()
cmd.execute()
self._undo.append(cmd)
- logger.debug("UndoStack: redid %s (depth=%d)", type(cmd).__name__, len(self._undo))
+ logger.debug(
+ "UndoStack: redid %s (depth=%d)", type(cmd).__name__, len(self._undo)
+ )
return True
def clear(self) -> None:
diff --git a/src/movement_optimizer/gui/comparison_dialog.py b/src/movement_optimizer/gui/comparison_dialog.py
index 92cd80e8e2..e220f6daea 100644
--- a/src/movement_optimizer/gui/comparison_dialog.py
+++ b/src/movement_optimizer/gui/comparison_dialog.py
@@ -63,7 +63,9 @@ def exec(self) -> None:
self.show()
def _build_metrics_table(self, metrics: list[dict]) -> str:
- lines = [f"{'Trial':<30} {'Ankle':>8} {'Knee':>8} {'Hip':>8} {'Work':>10} {'COM sway':>10}"]
+ lines = [
+ f"{'Trial':<30} {'Ankle':>8} {'Knee':>8} {'Hip':>8} {'Work':>10} {'COM sway':>10}"
+ ]
lines.append("-" * 80)
for m in metrics:
pt = m["peak_torques"]
diff --git a/src/movement_optimizer/gui/exercise_tab.py b/src/movement_optimizer/gui/exercise_tab.py
index 5401d753dd..a3633be9fa 100644
--- a/src/movement_optimizer/gui/exercise_tab.py
+++ b/src/movement_optimizer/gui/exercise_tab.py
@@ -145,7 +145,11 @@ def draw_all_plots(
if k != "anim":
self.axes[k].clear()
style_axis(self.axes[k])
- labels = Palette.BENCH_LABELS if exercise_type == "bench_press" else Palette.SEG_LABELS
+ labels = (
+ Palette.BENCH_LABELS
+ if exercise_type == "bench_press"
+ else Palette.SEG_LABELS
+ )
self._render_analysis_plots(result, body, bar_mass, labels)
self.fig.suptitle(
f"{self.name} | {body.body_mass:.0f} kg body, {bar_mass:.0f} kg barbell",
diff --git a/src/movement_optimizer/gui/file_operations.py b/src/movement_optimizer/gui/file_operations.py
index cf742a4e8d..b63166151f 100644
--- a/src/movement_optimizer/gui/file_operations.py
+++ b/src/movement_optimizer/gui/file_operations.py
@@ -240,13 +240,21 @@ def _export_excel(self: MainWindow) -> None:
if not path:
return
try:
- mass = getattr(body, "body_mass", None) # BodyModel uses body_mass, not mass
+ mass = getattr(
+ body, "body_mass", None
+ ) # BodyModel uses body_mass, not mass
height = getattr(body, "height", None)
export_to_excel(
- r, path, exercise_name=exercise_name, body_mass_kg=mass, body_height_m=height
+ r,
+ path,
+ exercise_name=exercise_name,
+ body_mass_kg=mass,
+ body_height_m=height,
)
self.status_label.setText(f"Exported: {os.path.basename(path)}")
- QMessageBox.information(self, "Exported", f"Excel workbook saved to:\n{path}")
+ QMessageBox.information(
+ self, "Exported", f"Excel workbook saved to:\n{path}"
+ )
except ImportError as e:
QMessageBox.critical(self, "Missing Dependency", str(e))
except (OSError, ValueError, RuntimeError) as e:
diff --git a/src/movement_optimizer/gui/help_dialog.py b/src/movement_optimizer/gui/help_dialog.py
index 844ab6ce13..357ba4ffd5 100644
--- a/src/movement_optimizer/gui/help_dialog.py
+++ b/src/movement_optimizer/gui/help_dialog.py
@@ -161,7 +161,9 @@ class HelpCenterDialog(QDialog):
),
}
- def __init__(self, parent: QWidget | None = None, initial_topic: str = "parameters") -> None:
+ def __init__(
+ self, parent: QWidget | None = None, initial_topic: str = "parameters"
+ ) -> None:
super().__init__(parent)
self.setWindowTitle("Movement Optimizer Help")
self.setMinimumWidth(680)
@@ -175,7 +177,9 @@ def _build_ui(self) -> None:
outer.setContentsMargins(12, 12, 12, 12)
outer.setSpacing(8)
- header = QLabel("Offline help for setup, parameters, results, troubleshooting, and terms.")
+ header = QLabel(
+ "Offline help for setup, parameters, results, troubleshooting, and terms."
+ )
header.setWordWrap(True)
outer.addWidget(header)
@@ -218,7 +222,9 @@ def _build_parameter_tab(self) -> QScrollArea:
lbl = QLabel(f"{heading}")
grid.addWidget(lbl, 0, col)
- for row, (name, (desc, unit, rng)) in enumerate(self.PARAMETERS.items(), start=1):
+ for row, (name, (desc, unit, rng)) in enumerate(
+ self.PARAMETERS.items(), start=1
+ ):
name_lbl = QLabel(name)
name_lbl.setAlignment(Qt.AlignmentFlag.AlignTop)
@@ -227,10 +233,14 @@ def _build_parameter_tab(self) -> QScrollArea:
desc_lbl.setAlignment(Qt.AlignmentFlag.AlignTop)
unit_lbl = QLabel(unit)
- unit_lbl.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
+ unit_lbl.setAlignment(
+ Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter
+ )
rng_lbl = QLabel(rng)
- rng_lbl.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter)
+ rng_lbl.setAlignment(
+ Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignHCenter
+ )
grid.addWidget(name_lbl, row, 0)
grid.addWidget(desc_lbl, row, 1)
diff --git a/src/movement_optimizer/gui/labelled_slider.py b/src/movement_optimizer/gui/labelled_slider.py
index 48d4e11a10..a5f6788911 100644
--- a/src/movement_optimizer/gui/labelled_slider.py
+++ b/src/movement_optimizer/gui/labelled_slider.py
@@ -41,7 +41,9 @@ def __init__(
row = QHBoxLayout()
self.name_label = QLabel(label)
self.val_label = QLabel(self._fmt(default))
- self.val_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+ self.val_label.setAlignment(
+ Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
+ )
row.addWidget(self.name_label)
row.addStretch()
row.addWidget(self.val_label)
@@ -56,7 +58,11 @@ def __init__(
suppress_wheel_events(self.slider)
layout.addWidget(self.slider)
- _tip = tooltip if tooltip else f"{label} ({lo:.{decimals}f}-{hi:.{decimals}f} {unit})"
+ _tip = (
+ tooltip
+ if tooltip
+ else f"{label} ({lo:.{decimals}f}-{hi:.{decimals}f} {unit})"
+ )
self.slider.setToolTip(_tip)
self.name_label.setToolTip(_tip)
diff --git a/src/movement_optimizer/gui/main_window.py b/src/movement_optimizer/gui/main_window.py
index c6c95e3237..aafc098ae8 100644
--- a/src/movement_optimizer/gui/main_window.py
+++ b/src/movement_optimizer/gui/main_window.py
@@ -82,7 +82,9 @@ class MainWindow(
# Signals for thread-safe GUI updates from the optimizer worker.
# Using signals instead of QTimer.singleShot is the Qt-correct way
# to communicate from a background thread to the main thread.
- _sig_done = pyqtSignal(int, object, object, float, object) # idx, result, body, bar, then_chain
+ _sig_done = pyqtSignal(
+ int, object, object, float, object
+ ) # idx, result, body, bar, then_chain
_sig_cancelled = pyqtSignal()
_sig_error = pyqtSignal(object) # MovementOptimizerError or str
_sig_progress = pyqtSignal(object) # ProgressReport
@@ -104,7 +106,9 @@ def __init__(self) -> None:
self.setMinimumSize(800, 600)
self.resize(1100, 700)
- self.exercise_states = [ExerciseRuntimeState() for _name, _etype in self.EXERCISE_CONFIGS]
+ self.exercise_states = [
+ ExerciseRuntimeState() for _name, _etype in self.EXERCISE_CONFIGS
+ ]
self.is_playing = False
self.anim_timer = QTimer(self)
self.anim_timer.timeout.connect(self._anim_step)
@@ -373,7 +377,9 @@ def _connect_slider_undo(self) -> None:
for name in slider_names:
labelled = getattr(self.sidebar, name, None)
if labelled is None:
- logger.warning("_connect_slider_undo: sidebar has no attribute %r", name)
+ logger.warning(
+ "_connect_slider_undo: sidebar has no attribute %r", name
+ )
continue
raw = labelled.slider
@@ -436,14 +442,18 @@ def _sync_motion_tab_controls(self, _index: int | None = None) -> None:
self._motion_tab_button_states.clear()
else:
if not self._motion_tab_button_states:
- self._motion_tab_button_states = {button: button.isEnabled() for button in buttons}
+ self._motion_tab_button_states = {
+ button: button.isEnabled() for button in buttons
+ }
for button in buttons:
button.setEnabled(False)
self.controls.setEnabled(True)
if enabled:
self.status_label.setText("Ready")
else:
- self.status_label.setText("Analysis tabs use local and bottom playback controls.")
+ self.status_label.setText(
+ "Analysis tabs use local and bottom playback controls."
+ )
self._sync_right_sidebar_toggle()
def _active_analysis_tab(self) -> Any | None:
diff --git a/src/movement_optimizer/gui/motion_analysis_panel.py b/src/movement_optimizer/gui/motion_analysis_panel.py
index 7edbf54175..afcc2b3c9f 100644
--- a/src/movement_optimizer/gui/motion_analysis_panel.py
+++ b/src/movement_optimizer/gui/motion_analysis_panel.py
@@ -57,7 +57,9 @@ def __init__(self, axis_names: Sequence[str], *, rows: int, cols: int) -> None:
self.figure = Figure(figsize=(8.0, 5.0), facecolor=Palette.BG)
self.canvas = FigureCanvasQTAgg(self.figure)
- self.canvas.setMinimumSize(self._minimum_canvas_width(), self._minimum_canvas_height())
+ self.canvas.setMinimumSize(
+ self._minimum_canvas_width(), self._minimum_canvas_height()
+ )
self.canvas.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding,
QSizePolicy.Policy.MinimumExpanding,
diff --git a/src/movement_optimizer/gui/motion_controls.py b/src/movement_optimizer/gui/motion_controls.py
index f9f23d93d3..93784893d8 100644
--- a/src/movement_optimizer/gui/motion_controls.py
+++ b/src/movement_optimizer/gui/motion_controls.py
@@ -47,7 +47,9 @@ def __init__(
self.slider.setRange(0, self._steps)
self.slider.setTracking(False)
self.slider.setMinimumHeight(28)
- self.slider.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
+ self.slider.setSizePolicy(
+ QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
+ )
self.edit = QLineEdit()
self.edit.setFixedWidth(88)
self.edit.setMinimumHeight(28)
@@ -89,7 +91,11 @@ def _sync_widgets(self) -> None:
self.slider.blockSignals(True)
self.slider.setValue(slider_value)
self.slider.blockSignals(False)
- text = f"{int(self._value)}" if self._integer else f"{self._value:.{self._decimals}f}"
+ text = (
+ f"{int(self._value)}"
+ if self._integer
+ else f"{self._value:.{self._decimals}f}"
+ )
if self.edit.text() != text:
self.edit.setText(text)
diff --git a/src/movement_optimizer/gui/motion_tabs.py b/src/movement_optimizer/gui/motion_tabs.py
index 5bca53b79a..50f98da3a8 100644
--- a/src/movement_optimizer/gui/motion_tabs.py
+++ b/src/movement_optimizer/gui/motion_tabs.py
@@ -120,19 +120,31 @@ def _swing_overlay_scene(
origin = (float(field.com_m[0]), float(field.com_m[1]))
if gravity:
gravity_vec = (float(field.gravity_n[0]), float(field.gravity_n[1]))
- arrows.append(ForceArrow(origin, gravity_vec, VectorStyle(LEG, label="gravity")))
+ arrows.append(
+ ForceArrow(origin, gravity_vec, VectorStyle(LEG, label="gravity"))
+ )
if tension:
tension_vec = (float(field.chain_tension_n[0]), float(field.chain_tension_n[1]))
- arrows.append(ForceArrow(origin, tension_vec, VectorStyle(CHAIN, label="tension")))
+ arrows.append(
+ ForceArrow(origin, tension_vec, VectorStyle(CHAIN, label="tension"))
+ )
if torque:
- for joint, magnitude in zip(SWING_POLICY_JOINT_NAMES, field.joint_torque_nm, strict=True):
+ for joint, magnitude in zip(
+ SWING_POLICY_JOINT_NAMES, field.joint_torque_nm, strict=True
+ ):
point = field.joint_points_m[joint]
arcs.append(
- TorqueArc((float(point[0]), float(point[1])), float(magnitude), VectorStyle(ARM))
+ TorqueArc(
+ (float(point[0]), float(point[1])),
+ float(magnitude),
+ VectorStyle(ARM),
+ )
)
if com:
markers.append(ComMarker(origin, VectorStyle(ACCENT)))
- return OverlayScene(arrows=tuple(arrows), torque_arcs=tuple(arcs), com_markers=tuple(markers))
+ return OverlayScene(
+ arrows=tuple(arrows), torque_arcs=tuple(arcs), com_markers=tuple(markers)
+ )
def _chain_overlay_scene(
@@ -145,7 +157,10 @@ def _chain_overlay_scene(
"""Build the chain overlay scene from a per-link force field, filtered by toggles."""
arrows: list[ForceArrow] = []
for index in range(len(field.midpoints_m)):
- origin = (float(field.midpoints_m[index][0]), float(field.midpoints_m[index][1]))
+ origin = (
+ float(field.midpoints_m[index][0]),
+ float(field.midpoints_m[index][1]),
+ )
if gravity:
vec = (float(field.gravity_n[index][0]), float(field.gravity_n[index][1]))
arrows.append(ForceArrow(origin, vec, VectorStyle(LEG)))
@@ -153,7 +168,10 @@ def _chain_overlay_scene(
vec = (float(field.tension_n[index][0]), float(field.tension_n[index][1]))
arrows.append(ForceArrow(origin, vec, VectorStyle(CHAIN)))
if net:
- vec = (float(field.net_force_n[index][0]), float(field.net_force_n[index][1]))
+ vec = (
+ float(field.net_force_n[index][0]),
+ float(field.net_force_n[index][1]),
+ )
arrows.append(ForceArrow(origin, vec, VectorStyle(ARM)))
return OverlayScene(arrows=tuple(arrows))
@@ -277,7 +295,8 @@ def _chain_path_length(self) -> float:
@staticmethod
def _compute_chain_path_length(chain_nodes: list[tuple[float, float]]) -> float:
distances = [
- np.hypot(end[0] - start[0], end[1] - start[1]) for start, end in pairwise(chain_nodes)
+ np.hypot(end[0] - start[0], end[1] - start[1])
+ for start, end in pairwise(chain_nodes)
]
return max(float(sum(distances)), 0.5)
@@ -654,7 +673,13 @@ def _build_body_group(self) -> QGroupBox:
tooltip="Rider arm segment length (upper arm and forearm).",
)
self._add_control(
- form, "arm_mass", "Arm segment kg", 0.2, 10.0, 2.0, tooltip="Rider arm segment mass."
+ form,
+ "arm_mass",
+ "Arm segment kg",
+ 0.2,
+ 10.0,
+ 2.0,
+ tooltip="Rider arm segment mass.",
)
return group
@@ -714,16 +739,30 @@ def _build_policy_group(self) -> QGroupBox:
integer=True,
tooltip="Time steps simulated per evaluation when cycles are not used (up to 2000).",
)
- self._add_control(form, "freq_min", "Freq min Hz", 0.2, 2.0, 0.45, refresh=False)
- self._add_control(form, "freq_max", "Freq max Hz", 0.2, 2.0, 0.75, refresh=False)
+ self._add_control(
+ form, "freq_min", "Freq min Hz", 0.2, 2.0, 0.45, refresh=False
+ )
+ self._add_control(
+ form, "freq_max", "Freq max Hz", 0.2, 2.0, 0.75, refresh=False
+ )
self._add_control(
form, "freq_samples", "Freq samples", 1, 8, 3, integer=True, refresh=False
)
- self._add_control(form, "hip_rate_min", "Hip min rad/s", 0.0, 3.0, 0.5, refresh=False)
- self._add_control(form, "hip_rate_max", "Hip max rad/s", 0.0, 3.0, 1.3, refresh=False)
- self._add_control(form, "hip_samples", "Hip samples", 1, 8, 2, integer=True, refresh=False)
- self._add_control(form, "torso_rate_min", "Torso min rad/s", 0.0, 3.0, 0.3, refresh=False)
- self._add_control(form, "torso_rate_max", "Torso max rad/s", 0.0, 3.0, 1.1, refresh=False)
+ self._add_control(
+ form, "hip_rate_min", "Hip min rad/s", 0.0, 3.0, 0.5, refresh=False
+ )
+ self._add_control(
+ form, "hip_rate_max", "Hip max rad/s", 0.0, 3.0, 1.3, refresh=False
+ )
+ self._add_control(
+ form, "hip_samples", "Hip samples", 1, 8, 2, integer=True, refresh=False
+ )
+ self._add_control(
+ form, "torso_rate_min", "Torso min rad/s", 0.0, 3.0, 0.3, refresh=False
+ )
+ self._add_control(
+ form, "torso_rate_max", "Torso max rad/s", 0.0, 3.0, 1.1, refresh=False
+ )
self._add_control(
form,
"torso_samples",
@@ -734,15 +773,28 @@ def _build_policy_group(self) -> QGroupBox:
integer=True,
refresh=False,
)
- self._add_control(form, "knee_ratio_min", "Knee ratio min", 0.0, 1.5, 0.25, refresh=False)
- self._add_control(form, "knee_ratio_max", "Knee ratio max", 0.0, 1.5, 0.65, refresh=False)
+ self._add_control(
+ form, "knee_ratio_min", "Knee ratio min", 0.0, 1.5, 0.25, refresh=False
+ )
+ self._add_control(
+ form, "knee_ratio_max", "Knee ratio max", 0.0, 1.5, 0.65, refresh=False
+ )
self._add_control(
form, "knee_samples", "Knee samples", 1, 8, 2, integer=True, refresh=False
)
self._add_control(
- form, "phase_samples", "Phase samples", 1, 12, 2, integer=True, refresh=False
+ form,
+ "phase_samples",
+ "Phase samples",
+ 1,
+ 12,
+ 2,
+ integer=True,
+ refresh=False,
+ )
+ self._add_control(
+ form, "speed", "Playback speed", 0.25, 4.0, 1.0, refresh=False
)
- self._add_control(form, "speed", "Playback speed", 0.25, 4.0, 1.0, refresh=False)
layout.addLayout(form)
return group
@@ -925,7 +977,10 @@ def _policy_bounds(self) -> CyclicPolicyBounds:
return CyclicPolicyBounds(
frequency_hz=(self._value("freq_min"), self._value("freq_max")),
hip_rate_rad_s=(self._value("hip_rate_min"), self._value("hip_rate_max")),
- torso_rate_rad_s=(self._value("torso_rate_min"), self._value("torso_rate_max")),
+ torso_rate_rad_s=(
+ self._value("torso_rate_min"),
+ self._value("torso_rate_max"),
+ ),
knee_ratio=(self._value("knee_ratio_min"), self._value("knee_ratio_max")),
)
@@ -989,15 +1044,23 @@ def _render_snapshot(self, snapshot: SwingSetSnapshot) -> None:
def _populate_analysis_panel(self) -> None:
if self._rollout is None:
return
- history = swing_force_history(self._config(), self._rollout, DEFAULT_POLICY_DT_S)
+ history = swing_force_history(
+ self._config(), self._rollout, DEFAULT_POLICY_DT_S
+ )
self._force_history = history
- self._force_fields = swing_force_fields(self._config(), self._rollout, DEFAULT_POLICY_DT_S)
+ self._force_fields = swing_force_fields(
+ self._config(), self._rollout, DEFAULT_POLICY_DT_S
+ )
panel = self.analysis_panel
panel.clear()
- plot_renderer.plot_swing_joint_torques(panel.axes["torques"], history, legend=False)
+ plot_renderer.plot_swing_joint_torques(
+ panel.axes["torques"], history, legend=False
+ )
plot_renderer.plot_swing_joint_power(panel.axes["power"], history, legend=False)
plot_renderer.plot_swing_angle(panel.axes["angle"], history, legend=False)
- plot_renderer.plot_swing_com_height(panel.axes["com_height"], history, legend=False)
+ plot_renderer.plot_swing_com_height(
+ panel.axes["com_height"], history, legend=False
+ )
plot_renderer.plot_swing_energy(panel.axes["energy"], history, legend=False)
plot_renderer.plot_swing_com_path(panel.axes["com_path"], history, legend=False)
self._apply_plot_legend_visibility()
diff --git a/src/movement_optimizer/gui/motion_tabs_chain.py b/src/movement_optimizer/gui/motion_tabs_chain.py
index ecf7118113..9a2bb24e18 100644
--- a/src/movement_optimizer/gui/motion_tabs_chain.py
+++ b/src/movement_optimizer/gui/motion_tabs_chain.py
@@ -119,10 +119,22 @@ def _build_ui(self) -> None:
tooltip="Number of links in the chain.",
)
self._add_control(
- form, "length", "Link length m", 0.03, 1.0, 0.18, tooltip="Length of each chain link."
+ form,
+ "length",
+ "Link length m",
+ 0.03,
+ 1.0,
+ 0.18,
+ tooltip="Length of each chain link.",
)
self._add_control(
- form, "mass", "Link mass kg", 0.01, 4.0, 0.12, tooltip="Mass of each chain link."
+ form,
+ "mass",
+ "Link mass kg",
+ 0.01,
+ 4.0,
+ 0.12,
+ tooltip="Mass of each chain link.",
)
self._add_control(
form,
@@ -238,7 +250,9 @@ def _build_ui(self) -> None:
form.addRow("Segment angles", self.angle_edit)
control_layout.addWidget(controls)
# The chain tab draws no articulated rider, so omit that layer.
- control_layout.addWidget(self._build_layers_group(["grid", "chain", "markers", "forces"]))
+ control_layout.addWidget(
+ self._build_layers_group(["grid", "chain", "markers", "forces"])
+ )
control_layout.addWidget(self._build_force_group())
row = QHBoxLayout()
simulate_button = QPushButton("Simulate Whip")
@@ -248,7 +262,9 @@ def _build_ui(self) -> None:
)
simulate_button.clicked.connect(self._simulate)
randomize_button = QPushButton("Randomize Start")
- randomize_button.setToolTip("Set a random 'wadded' starting configuration (seeded).")
+ randomize_button.setToolTip(
+ "Set a random 'wadded' starting configuration (seeded)."
+ )
randomize_button.clicked.connect(self._randomize_wadded_start)
self.play_button = QPushButton("Play")
self.play_button.setToolTip("Play or pause the simulated whip animation.")
@@ -319,18 +335,24 @@ def _config(self) -> ChainConfig:
def _state(self) -> ChainState:
config = self._config()
angles = (
- initial_catenary_angles(config.segment_count, self._angle_to_rad(self._value("sag")))
+ initial_catenary_angles(
+ config.segment_count, self._angle_to_rad(self._value("sag"))
+ )
if self.tie_segments.isChecked()
else self._typed_angles(config.segment_count)
)
- velocities = initial_tip_kick_velocities(config.segment_count, self._value("kick"))
+ velocities = initial_tip_kick_velocities(
+ config.segment_count, self._value("kick")
+ )
return ChainState(angles, velocities)
def _typed_angles(self, segment_count: int) -> np.ndarray:
raw = self.angle_edit.text().strip()
if not raw:
return np.zeros(segment_count, dtype=np.float64)
- values = np.asarray([float(part.strip()) for part in raw.split(",")], dtype=np.float64)
+ values = np.asarray(
+ [float(part.strip()) for part in raw.split(",")], dtype=np.float64
+ )
if values.size != segment_count:
raise ValueError(f"Expected {segment_count} segment angles")
return np.deg2rad(values) if self.use_degrees.isChecked() else values
@@ -347,14 +369,20 @@ def _randomize_wadded_start(self) -> None:
seed=int(self._value("random_seed")),
)
self.tie_segments.setChecked(False)
- values = np.rad2deg(state.angles_rad) if self.use_degrees.isChecked() else state.angles_rad
+ values = (
+ np.rad2deg(state.angles_rad)
+ if self.use_degrees.isChecked()
+ else state.angles_rad
+ )
self.angle_edit.setText(", ".join(f"{value:.4f}" for value in values))
self._refresh()
def _refresh_angle_placeholder(self) -> None:
unit = "degrees" if self.use_degrees.isChecked() else "radians"
self._controls["sag"].set_value(20.0 if self.use_degrees.isChecked() else 0.35)
- self._controls["random_span"].set_value(180.0 if self.use_degrees.isChecked() else np.pi)
+ self._controls["random_span"].set_value(
+ 180.0 if self.use_degrees.isChecked() else np.pi
+ )
self.angle_edit.setPlaceholderText(f"comma-separated {unit}, one per segment")
def _value(self, key: str) -> float:
@@ -417,19 +445,26 @@ def _simulate(self) -> None:
def _populate_analysis_panel(self) -> None:
if self._rollout is None:
return
- self._force_fields = chain_force_fields(self._config(), self._rollout, self._dt_s)
+ self._force_fields = chain_force_fields(
+ self._config(), self._rollout, self._dt_s
+ )
history = chain_force_history(self._config(), self._rollout, self._dt_s)
time_s = history.time_s
count = len(time_s)
panel = self.analysis_panel
panel.clear()
plot_renderer.plot_chain_tension(panel.axes["tension"], history, legend=False)
- plot_renderer.plot_chain_curvature(panel.axes["curvature"], history, legend=False)
+ plot_renderer.plot_chain_curvature(
+ panel.axes["curvature"], history, legend=False
+ )
plot_renderer.plot_chain_energy(
panel.axes["energy"], time_s, self._rollout.energy_j[:count], legend=False
)
plot_renderer.plot_chain_tip_speed(
- panel.axes["tip_speed"], time_s, self._rollout.tip_speed_m_s[:count], legend=False
+ panel.axes["tip_speed"],
+ time_s,
+ self._rollout.tip_speed_m_s[:count],
+ legend=False,
)
self._apply_plot_legend_visibility()
panel.draw()
@@ -460,7 +495,9 @@ def _current_force_field(self) -> ChainForceField:
if not 0 <= self._frame_index < frame_count:
raise RuntimeError("DbC Blocked: frame index is outside the rollout")
if self._force_fields is None or len(self._force_fields) != frame_count:
- self._force_fields = chain_force_fields(self._config(), self._rollout, self._dt_s)
+ self._force_fields = chain_force_fields(
+ self._config(), self._rollout, self._dt_s
+ )
return self._force_fields[self._frame_index]
def _toggle_playback(self) -> None:
@@ -485,7 +522,9 @@ def playback_step_forward(self) -> None:
return
self._timer.stop()
self.play_button.setText("Play")
- self._frame_index = min(self._frame_index + 1, self._rollout.positions.shape[0] - 1)
+ self._frame_index = min(
+ self._frame_index + 1, self._rollout.positions.shape[0] - 1
+ )
self._render_chain_frame()
self.playbackStateChanged.emit()
diff --git a/src/movement_optimizer/gui/optimization_mixin.py b/src/movement_optimizer/gui/optimization_mixin.py
index d0f8c19e7c..d6350cf017 100644
--- a/src/movement_optimizer/gui/optimization_mixin.py
+++ b/src/movement_optimizer/gui/optimization_mixin.py
@@ -14,7 +14,12 @@
from ..cli import EXERCISE_FACTORIES
from ..constants import trapezoid
-from ..errors import MovementOptimizerError, OptimizationError, PhysicsError, ValidationError
+from ..errors import (
+ MovementOptimizerError,
+ OptimizationError,
+ PhysicsError,
+ ValidationError,
+)
from ..models import BodyModel
from ..trajectory import (
CancelledError,
@@ -103,14 +108,18 @@ def _set_anim_frame(self, idx: int, frame: int) -> None:
with self._opt_lock:
self.exercise_states[idx].anim_frame = frame
- def _set_exercise_result(self, idx: int, result: OptimizationResult, *, frame: int = 0) -> None:
+ def _set_exercise_result(
+ self, idx: int, result: OptimizationResult, *, frame: int = 0
+ ) -> None:
"""Atomically publish an optimization result and reset playback frame."""
with self._opt_lock:
state = self.exercise_states[idx]
state.result = result
state.anim_frame = frame
- def _resolve_exercise_params(self, idx: int) -> tuple[Any, Any, str, float, float, float]:
+ def _resolve_exercise_params(
+ self, idx: int
+ ) -> tuple[Any, Any, str, float, float, float]:
body = self.sidebar.get_body_model()
bar, dur, smoothness = self.sidebar.get_optimization_params()
_, etype = self.EXERCISE_CONFIGS[idx]
@@ -245,7 +254,9 @@ def _opt_worker(self, idx: int, then_chain: list[int] | None) -> None:
validation_err = ValidationError(
f"Invalid parameters: {exc}",
error_code="VALIDATION_ERROR",
- suggestion=("Check that all body and exercise parameters are within valid ranges."),
+ suggestion=(
+ "Check that all body and exercise parameters are within valid ranges."
+ ),
)
self._sig_error.emit(validation_err)
except (RuntimeError, OSError) as exc:
@@ -297,7 +308,9 @@ def _on_done(
tab.draw_anim_frame(0, result, dyn, body, etype)
elapsed = result.elapsed_s
t_str = (
- f"{elapsed:.1f}s" if elapsed < 60 else f"{int(elapsed // 60)}m {elapsed % 60:.0f}s"
+ f"{elapsed:.1f}s"
+ if elapsed < 60
+ else f"{int(elapsed // 60)}m {elapsed % 60:.0f}s"
)
self.sidebar.set_progress_done(t_str, result.n_evals)
self._enable_post_run_buttons()
@@ -383,7 +396,9 @@ def _update_result_summary(
f" COM sway: {r.com_horizontal_range_cm:.1f} cm\n"
f" Balance: {balance_ok}"
)
- self.sidebar.set_result_label(f"{name} results:\n{joint_lines}\n Work: {work:>6.0f} J")
+ self.sidebar.set_result_label(
+ f"{name} results:\n{joint_lines}\n Work: {work:>6.0f} J"
+ )
def _on_err(self, err: object) -> None:
"""Handle optimizer errors (called from main thread via signal)."""
diff --git a/src/movement_optimizer/gui/parameter_sidebar.py b/src/movement_optimizer/gui/parameter_sidebar.py
index 9ae64d4789..bb505035ab 100644
--- a/src/movement_optimizer/gui/parameter_sidebar.py
+++ b/src/movement_optimizer/gui/parameter_sidebar.py
@@ -112,7 +112,9 @@ def is_3d_mode(self) -> bool:
"""Return True if the 3D model is selected."""
return self.model_combo.currentIndex() == 1
- def connect_action_handlers(self, handlers: Mapping[str, Callable[..., None]]) -> None:
+ def connect_action_handlers(
+ self, handlers: Mapping[str, Callable[..., None]]
+ ) -> None:
"""Connect sidebar action signals to handlers supplied by the main window."""
self.optimize_current.connect(handlers["optimize_current"])
self.optimize_both.connect(handlers["optimize_both"])
@@ -132,8 +134,12 @@ def show_optimizing(self) -> None:
_st.show_optimizing(self)
self.cancel_btn.setEnabled(True)
self.cancel_btn.setToolTip("Cancel the currently running optimization (Esc)")
- self.opt_btn.setToolTip("Optimization currently in progress. Please wait or cancel.")
- self.both_btn.setToolTip("Optimization currently in progress. Please wait or cancel.")
+ self.opt_btn.setToolTip(
+ "Optimization currently in progress. Please wait or cancel."
+ )
+ self.both_btn.setToolTip(
+ "Optimization currently in progress. Please wait or cancel."
+ )
def show_idle(self) -> None:
_st.show_idle(self)
@@ -236,7 +242,9 @@ def set_clear_comparison_available(self, available: bool) -> None:
"""Enable or disable the clear comparison action."""
self.clear_compare_btn.setEnabled(available)
if available:
- self.clear_compare_btn.setToolTip("Clear all trials currently saved for comparison")
+ self.clear_compare_btn.setToolTip(
+ "Clear all trials currently saved for comparison"
+ )
else:
self.clear_compare_btn.setToolTip("No trials currently saved to clear")
@@ -244,9 +252,13 @@ def set_cancellation_available(self, available: bool) -> None:
"""Enable or disable the cancellation action."""
self.cancel_btn.setEnabled(available)
if not available:
- self.cancel_btn.setToolTip("Cancellation already requested, shutting down safely...")
+ self.cancel_btn.setToolTip(
+ "Cancellation already requested, shutting down safely..."
+ )
else:
- self.cancel_btn.setToolTip("Cancel the currently running optimization (Esc)")
+ self.cancel_btn.setToolTip(
+ "Cancel the currently running optimization (Esc)"
+ )
def set_cancelling(self) -> None:
"""Immediately reflect cancellation in the UI and flush pending events.
@@ -262,5 +274,7 @@ def set_cancelling(self) -> None:
self.both_btn.setEnabled(False)
self.cancel_btn.setEnabled(False)
self.cancel_btn.setText("Canceling…")
- self.cancel_btn.setToolTip("Cancellation already requested, shutting down safely...")
+ self.cancel_btn.setToolTip(
+ "Cancellation already requested, shutting down safely..."
+ )
QApplication.processEvents()
diff --git a/src/movement_optimizer/gui/playback_controls.py b/src/movement_optimizer/gui/playback_controls.py
index b8a0af199c..663cefa419 100644
--- a/src/movement_optimizer/gui/playback_controls.py
+++ b/src/movement_optimizer/gui/playback_controls.py
@@ -6,7 +6,14 @@
from collections.abc import Callable, Mapping
from PyQt6.QtCore import Qt, pyqtSignal
-from PyQt6.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QPushButton, QSlider, QWidget
+from PyQt6.QtWidgets import (
+ QCheckBox,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QSlider,
+ QWidget,
+)
from movement_optimizer.gui.wheel_blocker import suppress_wheel_events
@@ -26,12 +33,16 @@ def __init__(self, parent: QWidget | None = None) -> None:
self.btn_rewind = QPushButton("Rewind")
self.btn_rewind.setAccessibleName("Rewind to start")
- self.btn_rewind.setAccessibleDescription("Move the animation to the first frame.")
+ self.btn_rewind.setAccessibleDescription(
+ "Move the animation to the first frame."
+ )
self.btn_rewind.setToolTip("Rewind to start (Home)")
self.btn_back = QPushButton("Back")
self.btn_back.setAccessibleName("Step backward one frame")
- self.btn_back.setAccessibleDescription("Move the animation backward by one frame.")
+ self.btn_back.setAccessibleDescription(
+ "Move the animation backward by one frame."
+ )
self.btn_back.setToolTip("Step backward one frame")
self.btn_play = QPushButton("Play")
@@ -64,7 +75,9 @@ def __init__(self, parent: QWidget | None = None) -> None:
self.speed_slider.setRange(1, 30)
self.speed_slider.setValue(10)
self.speed_slider.setFixedWidth(100)
- self.speed_slider.valueChanged.connect(lambda v: self.speed_changed.emit(v / 10.0))
+ self.speed_slider.valueChanged.connect(
+ lambda v: self.speed_changed.emit(v / 10.0)
+ )
suppress_wheel_events(self.speed_slider)
layout.addWidget(self.speed_slider)
@@ -85,7 +98,9 @@ def __init__(self, parent: QWidget | None = None) -> None:
self.frame_label = QLabel("")
layout.addWidget(self.frame_label)
- def connect_action_handlers(self, handlers: Mapping[str, Callable[..., None]]) -> None:
+ def connect_action_handlers(
+ self, handlers: Mapping[str, Callable[..., None]]
+ ) -> None:
"""Connect playback signals to handlers supplied by the owning window."""
self.play_toggled.connect(handlers["play_toggled"])
self.step_fwd.connect(handlers["step_fwd"])
@@ -122,7 +137,9 @@ def set_speed_multiplier_text(self, speed: float) -> None:
"""Display the current playback speed multiplier."""
self.speed_label.setText(f"{speed:.1f}x")
- def set_playback_status(self, current_frame: int, total_frames: int, speed: float) -> None:
+ def set_playback_status(
+ self, current_frame: int, total_frames: int, speed: float
+ ) -> None:
"""Update the frame and speed labels together."""
self.set_frame_position(current_frame, total_frames)
self.set_speed_multiplier_text(speed)
diff --git a/src/movement_optimizer/gui/plot_renderer.py b/src/movement_optimizer/gui/plot_renderer.py
index 9b4aebd262..36dadd3d11 100644
--- a/src/movement_optimizer/gui/plot_renderer.py
+++ b/src/movement_optimizer/gui/plot_renderer.py
@@ -38,7 +38,9 @@ def _legend_outside_plot(ax: Any, *, fontsize: int = 7, columns: int = 3) -> Any
)
-def plot_angles(ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS) -> None:
+def plot_angles(
+ ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS
+) -> None:
n_dof = min(r.q.shape[1], len(labels))
for j in range(n_dof):
ax.plot(
@@ -54,7 +56,9 @@ def plot_angles(ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABE
_legend_outside_plot(ax, fontsize=6, columns=n_dof)
-def plot_torques(ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS) -> None:
+def plot_torques(
+ ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS
+) -> None:
n_dof = min(r.torques.shape[1], len(labels))
for j in range(n_dof):
ax.plot(
@@ -71,7 +75,9 @@ def plot_torques(ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LAB
_legend_outside_plot(ax, fontsize=6, columns=n_dof)
-def plot_power(ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS) -> None:
+def plot_power(
+ ax: Any, r: OptimizationResult, labels: tuple = Palette.SEG_LABELS
+) -> None:
n_dof = min(r.power.shape[1], len(labels))
for j in range(n_dof):
ax.plot(
@@ -187,7 +193,12 @@ def plot_com_balance(ax: Any, r: OptimizationResult, body: BodyModel) -> None:
def plot_spine_loads(
- ax_comp: Any, ax_shear: Any, r: OptimizationResult, body: BodyModel, bar_mass: float, name: str
+ ax_comp: Any,
+ ax_shear: Any,
+ r: OptimizationResult,
+ body: BodyModel,
+ bar_mass: float,
+ name: str,
) -> None:
exercise_type = name.lower().replace(" ", "_")
if exercise_type == "bottoms_up_squat":
@@ -250,7 +261,9 @@ def _style_timeseries_axis(
_legend_outside_plot(ax, fontsize=legend_fontsize)
-def plot_swing_joint_torques(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
+def plot_swing_joint_torques(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
for j, name in enumerate(SWING_POLICY_JOINT_NAMES):
ax.plot(
history.time_s,
@@ -263,7 +276,9 @@ def plot_swing_joint_torques(ax: Any, history: SwingForceHistory, *, legend: boo
_style_timeseries_axis(ax, "Torque (N·m)", "Joint Torques", legend=legend)
-def plot_swing_joint_power(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
+def plot_swing_joint_power(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
for j, name in enumerate(SWING_POLICY_JOINT_NAMES):
ax.plot(
history.time_s,
@@ -285,7 +300,9 @@ def plot_swing_joint_power(ax: Any, history: SwingForceHistory, *, legend: bool
_style_timeseries_axis(ax, "Power (W)", "Joint Power", legend=legend)
-def plot_swing_angle(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
+def plot_swing_angle(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
ax.plot(
history.time_s,
np.degrees(history.swing_angle_rad),
@@ -297,17 +314,35 @@ def plot_swing_angle(ax: Any, history: SwingForceHistory, *, legend: bool = True
_style_timeseries_axis(ax, "Angle (deg)", "Swing Angle", legend=legend)
-def plot_swing_com_height(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
- ax.plot(history.time_s, history.com_height_m, color=Palette.GREEN, lw=2, label="COM height")
+def plot_swing_com_height(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
+ ax.plot(
+ history.time_s,
+ history.com_height_m,
+ color=Palette.GREEN,
+ lw=2,
+ label="COM height",
+ )
_style_timeseries_axis(ax, "Height (m)", "COM Height", legend=legend)
-def plot_swing_energy(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
- ax.plot(history.time_s, history.energy_j, color=Palette.ORANGE, lw=2, label="Swing energy")
+def plot_swing_energy(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
+ ax.plot(
+ history.time_s,
+ history.energy_j,
+ color=Palette.ORANGE,
+ lw=2,
+ label="Swing energy",
+ )
_style_timeseries_axis(ax, "Energy (J)", "Swing Energy", legend=legend)
-def plot_swing_com_path(ax: Any, history: SwingForceHistory, *, legend: bool = True) -> None:
+def plot_swing_com_path(
+ ax: Any, history: SwingForceHistory, *, legend: bool = True
+) -> None:
# com_path_m is (x, +y-down); negate y so "up" is up on the plot.
xs = history.com_path_m[:, 0]
ys = -history.com_path_m[:, 1]
@@ -321,9 +356,15 @@ def plot_swing_com_path(ax: Any, history: SwingForceHistory, *, legend: bool = T
_legend_outside_plot(ax, fontsize=6)
-def plot_chain_tension(ax: Any, history: ChainForceHistory, *, legend: bool = True) -> None:
+def plot_chain_tension(
+ ax: Any, history: ChainForceHistory, *, legend: bool = True
+) -> None:
ax.plot(
- history.time_s, history.max_tension_n, color=Palette.RED, lw=2, label="Max link tension"
+ history.time_s,
+ history.max_tension_n,
+ color=Palette.RED,
+ lw=2,
+ label="Max link tension",
)
mean_tension = (
np.mean(history.link_tension_n, axis=1)
@@ -331,12 +372,19 @@ def plot_chain_tension(ax: Any, history: ChainForceHistory, *, legend: bool = Tr
else np.zeros_like(history.time_s)
)
ax.plot(
- history.time_s, mean_tension, color=Palette.ACCENT, lw=1.5, alpha=0.8, label="Mean tension"
+ history.time_s,
+ mean_tension,
+ color=Palette.ACCENT,
+ lw=1.5,
+ alpha=0.8,
+ label="Mean tension",
)
_style_timeseries_axis(ax, "Tension (N)", "Chain Link Tension", legend=legend)
-def plot_chain_curvature(ax: Any, history: ChainForceHistory, *, legend: bool = True) -> None:
+def plot_chain_curvature(
+ ax: Any, history: ChainForceHistory, *, legend: bool = True
+) -> None:
ax.plot(
history.time_s,
np.degrees(history.max_curvature_rad),
@@ -347,11 +395,15 @@ def plot_chain_curvature(ax: Any, history: ChainForceHistory, *, legend: bool =
_style_timeseries_axis(ax, "Curvature (deg)", "Chain Curvature", legend=legend)
-def plot_chain_energy(ax: Any, time_s: Any, energy_j: Any, *, legend: bool = True) -> None:
+def plot_chain_energy(
+ ax: Any, time_s: Any, energy_j: Any, *, legend: bool = True
+) -> None:
ax.plot(time_s, energy_j, color=Palette.GREEN, lw=2, label="Total energy")
_style_timeseries_axis(ax, "Energy (J)", "Chain Energy", legend=legend)
-def plot_chain_tip_speed(ax: Any, time_s: Any, tip_speed_m_s: Any, *, legend: bool = True) -> None:
+def plot_chain_tip_speed(
+ ax: Any, time_s: Any, tip_speed_m_s: Any, *, legend: bool = True
+) -> None:
ax.plot(time_s, tip_speed_m_s, color=Palette.BLUE, lw=2, label="Tip speed")
_style_timeseries_axis(ax, "Speed (m/s)", "Chain Tip Speed", legend=legend)
diff --git a/src/movement_optimizer/gui/policy_trace_canvas.py b/src/movement_optimizer/gui/policy_trace_canvas.py
index 68f79ed160..4e588ad614 100644
--- a/src/movement_optimizer/gui/policy_trace_canvas.py
+++ b/src/movement_optimizer/gui/policy_trace_canvas.py
@@ -93,7 +93,9 @@ def legend_visible(self) -> bool:
def _top_margin(self) -> float:
"""Top inset for the plotted series, reserving room for the legend."""
- return float(self._legend_band_height() if self._legend_visible else self._MARGIN_PX)
+ return float(
+ self._legend_band_height() if self._legend_visible else self._MARGIN_PX
+ )
@staticmethod
def _legend_entries() -> tuple[tuple[str, QColor], ...]:
@@ -152,7 +154,9 @@ def _axis_label_band_height(self) -> int:
"""Return reserved bottom height for the trace x-axis label."""
metrics = QFontMetrics(self.font())
return (
- self._AXIS_LABEL_TOP_PADDING_PX + metrics.height() + self._AXIS_LABEL_BOTTOM_PADDING_PX
+ self._AXIS_LABEL_TOP_PADDING_PX
+ + metrics.height()
+ + self._AXIS_LABEL_BOTTOM_PADDING_PX
)
def _minimum_height_for_width(self, width: int) -> int:
@@ -251,13 +255,18 @@ def _draw_legend(self, painter: QPainter) -> None:
y = baseline
for label, color in self._legend_entries():
item_width = self._legend_item_width(label)
- if x > self._MARGIN_PX and x + item_width > self._MARGIN_PX + available_width:
+ if (
+ x > self._MARGIN_PX
+ and x + item_width > self._MARGIN_PX + available_width
+ ):
x = self._MARGIN_PX
y += self._LEGEND_ROW_HEIGHT_PX
painter.setPen(QPen(color, 2))
painter.drawLine(x, y - 4, x + 12, y - 4)
painter.setPen(QPen(color, 1))
- painter.drawText(x + self._LEGEND_LINE_PX + self._LEGEND_TEXT_GAP_PX, y, label)
+ painter.drawText(
+ x + self._LEGEND_LINE_PX + self._LEGEND_TEXT_GAP_PX, y, label
+ )
x += item_width
def _iteration_label_rect(self) -> QRect:
@@ -282,7 +291,9 @@ def _build_series(
return {
"score_m": _trace_series(samples, lambda sample: sample.score_m),
"best_score_m": _trace_series(samples, lambda sample: sample.best_score_m),
- "frequency_hz": _trace_series(samples, lambda sample: sample.parameters.frequency_hz),
+ "frequency_hz": _trace_series(
+ samples, lambda sample: sample.parameters.frequency_hz
+ ),
"hip_rate_amplitude_rad_s": _trace_series(
samples, lambda sample: sample.parameters.hip_rate_amplitude_rad_s
),
@@ -292,7 +303,9 @@ def _build_series(
"knee_rate_ratio": _trace_series(
samples, lambda sample: sample.parameters.knee_rate_ratio
),
- "phase_rad": _trace_series(samples, lambda sample: sample.parameters.phase_rad),
+ "phase_rad": _trace_series(
+ samples, lambda sample: sample.parameters.phase_rad
+ ),
}
diff --git a/src/movement_optimizer/gui/session_state.py b/src/movement_optimizer/gui/session_state.py
index a9cd6f84d3..207878c5c2 100644
--- a/src/movement_optimizer/gui/session_state.py
+++ b/src/movement_optimizer/gui/session_state.py
@@ -42,7 +42,9 @@ def collect_slider_values(sidebar: ParameterSidebar) -> dict[str, float]:
}
-def restore_slider_values(sidebar: ParameterSidebar, slider_values: dict[str, float]) -> None:
+def restore_slider_values(
+ sidebar: ParameterSidebar, slider_values: dict[str, float]
+) -> None:
"""Apply persisted slider values to the GUI sidebar."""
slider_map = {
"body_mass": sidebar.mass_slider,
diff --git a/src/movement_optimizer/gui/vector_overlay.py b/src/movement_optimizer/gui/vector_overlay.py
index b7e459da18..074c1d775c 100644
--- a/src/movement_optimizer/gui/vector_overlay.py
+++ b/src/movement_optimizer/gui/vector_overlay.py
@@ -98,7 +98,9 @@ def auto_scale_factor(arrows: Sequence[ForceArrow], target_world_len: float) ->
return target_world_len / largest
-def _draw_arrowhead(painter: QPainter, tail: QPointF, tip: QPointF, head_px: float) -> None:
+def _draw_arrowhead(
+ painter: QPainter, tail: QPointF, tip: QPointF, head_px: float
+) -> None:
dx = tip.x() - tail.x()
dy = tip.y() - tail.y()
length = math.hypot(dx, dy)
@@ -219,6 +221,8 @@ def draw_overlay_scene(
if scene.arrows:
draw_force_arrows(painter, projector, scene.arrows, scale=arrow_scale)
if scene.torque_arcs:
- draw_torque_arcs(painter, projector, scene.torque_arcs, reference_nm=torque_reference_nm)
+ draw_torque_arcs(
+ painter, projector, scene.torque_arcs, reference_nm=torque_reference_nm
+ )
if scene.com_markers:
draw_com_markers(painter, projector, scene.com_markers)
diff --git a/src/movement_optimizer/import_results.py b/src/movement_optimizer/import_results.py
index 36b6cadf2a..7b18772733 100644
--- a/src/movement_optimizer/import_results.py
+++ b/src/movement_optimizer/import_results.py
@@ -49,12 +49,16 @@ def import_result_from_json(path: str | Path) -> dict:
raise ValueError(f"Invalid JSON in result file {path}: {exc}") from exc
if not isinstance(data, dict):
- raise ValueError(f"Invalid result file: expected JSON object, got {type(data).__name__}")
+ raise ValueError(
+ f"Invalid result file: expected JSON object, got {type(data).__name__}"
+ )
version = data.get("format_version")
if version is None:
# Legacy file without version -- try to load with a warning.
- logger.warning("Result file %s has no format_version; attempting legacy load", path)
+ logger.warning(
+ "Result file %s has no format_version; attempting legacy load", path
+ )
elif version != EXPORT_FORMAT_VERSION:
raise ValueError(
f"Incompatible format_version '{version}' in {path}; expected '{EXPORT_FORMAT_VERSION}'"
diff --git a/src/movement_optimizer/models/__init__.py b/src/movement_optimizer/models/__init__.py
index 1d18fd6f1a..6aad8c4695 100644
--- a/src/movement_optimizer/models/__init__.py
+++ b/src/movement_optimizer/models/__init__.py
@@ -72,7 +72,9 @@
from .swingset import cyclic_policy_controls as cyclic_policy_controls
from .swingset import estimate_swingset_joint_torques as estimate_swingset_joint_torques
from .swingset import optimize_cyclic_policy as optimize_cyclic_policy
-from .swingset import optimize_cyclic_policy_iterative as optimize_cyclic_policy_iterative
+from .swingset import (
+ optimize_cyclic_policy_iterative as optimize_cyclic_policy_iterative,
+)
from .swingset import simulate_swingset as simulate_swingset
from .swingset import simulate_swingset_controls as simulate_swingset_controls
from .swingset_forces import SwingForceField as SwingForceField
diff --git a/src/movement_optimizer/models/bilateral_3d.py b/src/movement_optimizer/models/bilateral_3d.py
index 983d7c8d83..27bf73cb4a 100644
--- a/src/movement_optimizer/models/bilateral_3d.py
+++ b/src/movement_optimizer/models/bilateral_3d.py
@@ -180,7 +180,10 @@ def _sagittal_step(
"""
# Performance optimization: Skip intermediate array allocation
return np.array(
- [origin_xz[0] + length * np.sin(angle), origin_xz[1] + length * np.cos(angle)]
+ [
+ origin_xz[0] + length * np.sin(angle),
+ origin_xz[1] + length * np.cos(angle),
+ ]
)
def forward_kinematics(self, pose: Bilateral3DPose) -> dict[str, NDArray]:
diff --git a/src/movement_optimizer/models/chain_dynamics.py b/src/movement_optimizer/models/chain_dynamics.py
index 49829c4929..1674a7bae8 100644
--- a/src/movement_optimizer/models/chain_dynamics.py
+++ b/src/movement_optimizer/models/chain_dynamics.py
@@ -195,7 +195,9 @@ def initial_catenary_angles(segment_count: int, sag_rad: float) -> FloatArray:
return np.linspace(-sag_rad, sag_rad, segment_count, dtype=np.float64)
-def initial_tip_kick_velocities(segment_count: int, amplitude_rad_s: float) -> FloatArray:
+def initial_tip_kick_velocities(
+ segment_count: int, amplitude_rad_s: float
+) -> FloatArray:
"""Return a smooth initial angular-velocity profile concentrated at the tip.
Preconditions:
@@ -231,8 +233,12 @@ def random_wadded_chain_state(
raise ValueError("velocity_span_rad_s must be non-negative")
rng = np.random.default_rng(seed)
angles = rng.uniform(-angle_span_rad, angle_span_rad, config.segment_count)
- velocities = rng.uniform(-velocity_span_rad_s, velocity_span_rad_s, config.segment_count)
- return ChainState(angles.astype(np.float64), velocities.astype(np.float64)).validated(config)
+ velocities = rng.uniform(
+ -velocity_span_rad_s, velocity_span_rad_s, config.segment_count
+ )
+ return ChainState(
+ angles.astype(np.float64), velocities.astype(np.float64)
+ ).validated(config)
def _angular_acceleration(
@@ -266,7 +272,11 @@ def _angular_acceleration(
)
bend_damping_torque = config.bend_damping * neighbor_velocity_sum
return (
- gravity_torque + damping_torque + coupling_torque + bend_damping_torque + torques
+ gravity_torque
+ + damping_torque
+ + coupling_torque
+ + bend_damping_torque
+ + torques
) / inertia
diff --git a/src/movement_optimizer/models/chain_forces.py b/src/movement_optimizer/models/chain_forces.py
index b4006a9d14..dca8cdde6f 100644
--- a/src/movement_optimizer/models/chain_forces.py
+++ b/src/movement_optimizer/models/chain_forces.py
@@ -60,7 +60,9 @@ class ChainForceHistory:
def _gravity_vector(config: ChainConfig) -> FloatArray:
"""Per-link weight vector (points toward +y, the model's downward axis)."""
- return np.asarray([0.0, config.link_mass_kg * config.gravity_m_s2], dtype=np.float64)
+ return np.asarray(
+ [0.0, config.link_mass_kg * config.gravity_m_s2], dtype=np.float64
+ )
def _midpoint_velocities(config: ChainConfig, rollout: ChainRollout) -> FloatArray:
@@ -72,7 +74,9 @@ def _midpoint_velocities(config: ChainConfig, rollout: ChainRollout) -> FloatArr
return np.stack(per_state)
-def link_accelerations(config: ChainConfig, rollout: ChainRollout, dt_s: float) -> FloatArray:
+def link_accelerations(
+ config: ChainConfig, rollout: ChainRollout, dt_s: float
+) -> FloatArray:
"""Return ``(T, N, 2)`` link-midpoint linear accelerations via finite difference.
Preconditions:
diff --git a/src/movement_optimizer/models/lagrangian_balance.py b/src/movement_optimizer/models/lagrangian_balance.py
index 83ec2a36b1..b98133550a 100644
--- a/src/movement_optimizer/models/lagrangian_balance.py
+++ b/src/movement_optimizer/models/lagrangian_balance.py
@@ -111,7 +111,9 @@ def residual(angle: float) -> float:
return q
-def _standing_balanced(dyn: _DynamicsWithBody, bar_mass: float, exercise_type: str) -> NDArray:
+def _standing_balanced(
+ dyn: _DynamicsWithBody, bar_mass: float, exercise_type: str
+) -> NDArray:
"""Find a near-standing pose with COM at inner BOS center.
Adjusts shin angle (joint 0) to shift COM forward over mid-foot.
diff --git a/src/movement_optimizer/models/lagrangian_dynamics.py b/src/movement_optimizer/models/lagrangian_dynamics.py
index ec8706c4f0..77b3e12f3c 100644
--- a/src/movement_optimizer/models/lagrangian_dynamics.py
+++ b/src/movement_optimizer/models/lagrangian_dynamics.py
@@ -375,7 +375,9 @@ def _batch_gravity_torques(self, q: NDArray) -> NDArray:
supine=self.supine,
)
- def _numpy_inverse_dynamics_batch(self, q: NDArray, qd: NDArray, qdd: NDArray) -> NDArray:
+ def _numpy_inverse_dynamics_batch(
+ self, q: NDArray, qd: NDArray, qdd: NDArray
+ ) -> NDArray:
"""NumPy fallback — delegates to :func:`lagrangian_batch.numpy_inverse_dynamics_batch`."""
return numpy_inverse_dynamics_batch(
q,
@@ -409,7 +411,9 @@ def inverse_dynamics_batch(self, q: NDArray, qd: NDArray, qdd: NDArray) -> NDArr
Rust and NumPy paths have the same asymptotic complexity.
"""
self._require_finite_batch_inputs(q, qd, qdd)
- self._check_coriolis_slow_assumption(float(np.max(np.abs(qd))) if qd.size else 0.0)
+ self._check_coriolis_slow_assumption(
+ float(np.max(np.abs(qd))) if qd.size else 0.0
+ )
try:
from movement_optimizer_core import inverse_dynamics_batch_rs # type: ignore[import-not-found] # noqa: I001
diff --git a/src/movement_optimizer/models/lagrangian_kinematics.py b/src/movement_optimizer/models/lagrangian_kinematics.py
index 61dddab0d0..5ae315759c 100644
--- a/src/movement_optimizer/models/lagrangian_kinematics.py
+++ b/src/movement_optimizer/models/lagrangian_kinematics.py
@@ -131,14 +131,21 @@ def _numpy_com_x_batch(
c3x = hip_x + d[2] * sq[:, 2]
total_mass = b.body_mass + bar_mass
- numerator = b.m_feet * b.foot_com_x + self.m[0] * c1x + self.m[1] * c2x + self.m[2] * c3x
+ numerator = (
+ b.m_feet * b.foot_com_x
+ + self.m[0] * c1x
+ + self.m[1] * c2x
+ + self.m[2] * c3x
+ )
if exercise_type in ("squat", "full_squat"):
if hasattr(b, "squat_bar_depth") and (
b.squat_bar_depth != 0.0 or b.squat_bar_height != 0.0
):
bar_x = (
- shoulder_x - b.squat_bar_height * sq[:, 2] - b.squat_bar_depth * np.cos(q[:, 2])
+ shoulder_x
+ - b.squat_bar_height * sq[:, 2]
+ - b.squat_bar_depth * np.cos(q[:, 2])
)
else:
bar_x = shoulder_x
@@ -269,8 +276,18 @@ def com_position(
total_mass = b.body_mass + bar_mass
- num_x = b.m_feet * b.foot_com_x + self.m[0] * c1_x + self.m[1] * c2_x + self.m[2] * c3_x
- num_y = b.m_feet * b.foot_com_y + self.m[0] * c1_y + self.m[1] * c2_y + self.m[2] * c3_y
+ num_x = (
+ b.m_feet * b.foot_com_x
+ + self.m[0] * c1_x
+ + self.m[1] * c2_x
+ + self.m[2] * c3_x
+ )
+ num_y = (
+ b.m_feet * b.foot_com_y
+ + self.m[0] * c1_y
+ + self.m[1] * c2_y
+ + self.m[2] * c3_y
+ )
if exercise_type in ("squat", "full_squat"):
bar_pos = self.bar_position(q, exercise_type)
diff --git a/src/movement_optimizer/models/swingset.py b/src/movement_optimizer/models/swingset.py
index d2ca3ac2d2..5fdb506c77 100644
--- a/src/movement_optimizer/models/swingset.py
+++ b/src/movement_optimizer/models/swingset.py
@@ -20,7 +20,9 @@
FloatArray: TypeAlias = NDArray[np.float64]
Policy: TypeAlias = Callable[["SwingSetState", float], "SwingControlAction"]
-ProgressCallback: TypeAlias = Callable[[int, int, float, "CyclicPolicyParameters"], None]
+ProgressCallback: TypeAlias = Callable[
+ [int, int, float, "CyclicPolicyParameters"], None
+]
DEFAULT_CHAIN_SEGMENTS: Final[int] = 14
DEFAULT_CHAIN_LENGTH_M: Final[float] = 2.4
@@ -309,8 +311,12 @@ class CyclicPolicySearchSpace:
def __post_init__(self) -> None:
_require_range("frequency_hz", self.frequency_hz_min, self.frequency_hz_max)
- _require_range("hip_rate_rad_s", self.hip_rate_min_rad_s, self.hip_rate_max_rad_s)
- _require_range("torso_rate_rad_s", self.torso_rate_min_rad_s, self.torso_rate_max_rad_s)
+ _require_range(
+ "hip_rate_rad_s", self.hip_rate_min_rad_s, self.hip_rate_max_rad_s
+ )
+ _require_range(
+ "torso_rate_rad_s", self.torso_rate_min_rad_s, self.torso_rate_max_rad_s
+ )
_require_range("knee_ratio", self.knee_ratio_min, self.knee_ratio_max)
for name, value in (
("frequency_samples", self.frequency_samples),
@@ -350,7 +356,9 @@ def __post_init__(self) -> None:
if phase_lower < 0.0:
raise ValueError("phase_rad_min must be non-negative")
if phase_upper < phase_lower:
- raise ValueError("phase_rad_max must be greater than or equal to phase_rad_min")
+ raise ValueError(
+ "phase_rad_max must be greater than or equal to phase_rad_min"
+ )
def as_list(self) -> list[tuple[float, float]]:
"""Return bounds ordered to match the optimizer parameter vector."""
@@ -460,7 +468,11 @@ def _arm_elbow_point(
forearm_length = config.forearm.length_m
delta = hand - shoulder
distance = float(np.linalg.norm(delta))
- unit = delta / distance if distance > 1e-9 else np.asarray([0.0, 1.0], dtype=np.float64)
+ unit = (
+ delta / distance
+ if distance > 1e-9
+ else np.asarray([0.0, 1.0], dtype=np.float64)
+ )
minimum_reach = abs(upper_length - forearm_length) + 1e-9
maximum_reach = upper_length + forearm_length - 1e-9
effective_distance = _clamp(distance, minimum_reach, maximum_reach)
@@ -489,7 +501,9 @@ def _elbow_offset_bias(elbow_bias_rad: float) -> float:
``elbow_bias_rad`` is finite.
"""
- clamped = constrain_swing_pose(SwingPose(elbow_angle_rad=elbow_bias_rad)).elbow_angle_rad
+ clamped = constrain_swing_pose(
+ SwingPose(elbow_angle_rad=elbow_bias_rad)
+ ).elbow_angle_rad
lower, upper = SWING_ELBOW_LIMITS_RAD
span = upper - lower
if span <= 0.0:
@@ -641,7 +655,9 @@ def _policy(_state: SwingSetState, time_s: float) -> SwingControlAction:
torso_lean_rate_rad_s=-parameters.torso_rate_amplitude_rad_s * driver,
hip_rate_rad_s=parameters.hip_rate_amplitude_rad_s * driver,
knee_rate_rad_s=(
- -parameters.knee_rate_ratio * parameters.hip_rate_amplitude_rad_s * driver
+ -parameters.knee_rate_ratio
+ * parameters.hip_rate_amplitude_rad_s
+ * driver
),
shoulder_rate_rad_s=-0.1 * driver,
elbow_rate_rad_s=0.12 * driver,
@@ -666,7 +682,9 @@ def cyclic_policy_controls(
raise ValueError("steps must be at least 1")
_require_positive("dt_s", dt_s)
times = np.arange(steps, dtype=np.float64) * dt_s
- driver = np.sin(2.0 * np.pi * parameters.frequency_hz * times + parameters.phase_rad)
+ driver = np.sin(
+ 2.0 * np.pi * parameters.frequency_hz * times + parameters.phase_rad
+ )
return np.column_stack(
(
-parameters.torso_rate_amplitude_rad_s * driver,
@@ -710,7 +728,9 @@ def simulate_swingset_controls(
or control_array.shape[1] != CONTROL_DIMENSION
or not np.all(np.isfinite(control_array))
):
- raise ValueError("controls must have shape (N >= 1, 5) and contain finite values")
+ raise ValueError(
+ "controls must have shape (N >= 1, 5) and contain finite values"
+ )
_require_positive("dt_s", dt_s)
states = [replace(initial_state, pose=constrain_swing_pose(initial_state.pose))]
snapshots = [build_swingset_snapshot(config, initial_state.pose)]
@@ -805,7 +825,9 @@ def optimize_cyclic_policy(
best_params = parameters
best_rollout = rollout
best_score = score
- if best_rollout is None: # pragma: no cover - defensive guard for malformed searches.
+ if (
+ best_rollout is None
+ ): # pragma: no cover - defensive guard for malformed searches.
raise RuntimeError("Policy search did not evaluate a rollout")
trace.append(
CyclicPolicyTraceSample(
@@ -818,7 +840,9 @@ def optimize_cyclic_policy(
)
if progress_callback is not None:
progress_callback(index, len(candidates), best_score, best_params)
- if best_rollout is None: # pragma: no cover - defensive guard for malformed searches.
+ if (
+ best_rollout is None
+ ): # pragma: no cover - defensive guard for malformed searches.
raise RuntimeError("Policy search did not evaluate a rollout")
return CyclicPolicySearchResult(
best_params,
@@ -830,7 +854,9 @@ def optimize_cyclic_policy(
)
-def _params_from_vector(vector: FloatArray, bounds: CyclicPolicyBounds) -> CyclicPolicyParameters:
+def _params_from_vector(
+ vector: FloatArray, bounds: CyclicPolicyBounds
+) -> CyclicPolicyParameters:
"""Build clamped policy parameters from an optimizer vector.
Clamping matters because the local-refinement stage (Nelder-Mead) is not
@@ -838,7 +864,8 @@ def _params_from_vector(vector: FloatArray, bounds: CyclicPolicyBounds) -> Cycli
"""
limits = bounds.as_list()
clamped = [
- _clamp(float(value), low, high) for value, (low, high) in zip(vector, limits, strict=True)
+ _clamp(float(value), low, high)
+ for value, (low, high) in zip(vector, limits, strict=True)
]
return CyclicPolicyParameters(
frequency_hz=clamped[0],
@@ -963,7 +990,9 @@ def _objective(vector: FloatArray) -> float:
options={"maxfev": budget - eval_count, "xatol": 1e-4, "fatol": 1e-6},
)
- if best_rollout is None or best_params is None: # pragma: no cover - budget>=1 guarantees one.
+ if (
+ best_rollout is None or best_params is None
+ ): # pragma: no cover - budget>=1 guarantees one.
raise RuntimeError("Iterative policy search did not evaluate a rollout")
return CyclicPolicySearchResult(
best_params,
@@ -994,7 +1023,9 @@ def estimate_swingset_joint_torques(
return np.zeros((0, CONTROL_DIMENSION), dtype=np.float64)
inertias = _policy_joint_inertias(config)
accelerations = (
- np.gradient(controls, dt_s, axis=0) if controls.shape[0] > 1 else np.zeros_like(controls)
+ np.gradient(controls, dt_s, axis=0)
+ if controls.shape[0] > 1
+ else np.zeros_like(controls)
)
damping = 0.08 * inertias * controls
return accelerations * inertias + damping
@@ -1004,12 +1035,14 @@ def _policy_joint_inertias(config: SwingSetConfig) -> FloatArray:
torso = config.torso.mass_kg * config.torso.length_m**2 / 3.0
hip = 2.0 * (
config.thigh.mass_kg * config.thigh.length_m**2 / 3.0
- + config.shank.mass_kg * (config.thigh.length_m + 0.5 * config.shank.length_m) ** 2
+ + config.shank.mass_kg
+ * (config.thigh.length_m + 0.5 * config.shank.length_m) ** 2
)
knee = 2.0 * config.shank.mass_kg * config.shank.length_m**2 / 3.0
shoulder = 2.0 * (
config.upper_arm.mass_kg * config.upper_arm.length_m**2 / 3.0
- + config.forearm.mass_kg * (config.upper_arm.length_m + 0.5 * config.forearm.length_m) ** 2
+ + config.forearm.mass_kg
+ * (config.upper_arm.length_m + 0.5 * config.forearm.length_m) ** 2
)
elbow = 2.0 * config.forearm.mass_kg * config.forearm.length_m**2 / 3.0
return np.asarray([torso, hip, knee, shoulder, elbow], dtype=np.float64)
diff --git a/src/movement_optimizer/models/swingset_forces.py b/src/movement_optimizer/models/swingset_forces.py
index 8a653352e1..ca3d4f9261 100644
--- a/src/movement_optimizer/models/swingset_forces.py
+++ b/src/movement_optimizer/models/swingset_forces.py
@@ -122,7 +122,9 @@ def swing_force_fields(
torque_index = min(frame_index, torques.shape[0] - 1)
chain_tension = mass * accelerations[frame_index] - gravity_vec
joint_points = {
- joint: np.asarray(snapshot.points[_JOINT_POINT_KEYS[joint]], dtype=np.float64)
+ joint: np.asarray(
+ snapshot.points[_JOINT_POINT_KEYS[joint]], dtype=np.float64
+ )
for joint in SWING_POLICY_JOINT_NAMES
}
fields.append(
diff --git a/src/movement_optimizer/persistence.py b/src/movement_optimizer/persistence.py
index a785d728a0..9c82b07317 100644
--- a/src/movement_optimizer/persistence.py
+++ b/src/movement_optimizer/persistence.py
@@ -109,7 +109,9 @@ class InvalidStateFileError(ValueError):
def _require_mapping(data: Any, context: str) -> dict[str, Any]:
"""Return ``data`` as a dict or raise with a descriptive context."""
if not isinstance(data, dict):
- raise InvalidStateFileError(f"{context}: expected JSON object, got {type(data).__name__}")
+ raise InvalidStateFileError(
+ f"{context}: expected JSON object, got {type(data).__name__}"
+ )
return data
@@ -142,7 +144,9 @@ def _require_type(value: Any, expected: type | tuple[type, ...], field: str) ->
def _require_range(value: float, bounds: tuple[float, float], field: str) -> None:
low, high = bounds
if not (low <= value <= high):
- raise InvalidStateFileError(f"field '{field}': value {value} out of range [{low}, {high}]")
+ raise InvalidStateFileError(
+ f"field '{field}': value {value} out of range [{low}, {high}]"
+ )
def _validate_schema_version(data: dict[str, Any], context: str) -> None:
@@ -185,7 +189,9 @@ def _validate_metadata_block(metadata: Any, context: str) -> None:
}
for key, expected in required_types.items():
if key not in meta_dict:
- raise InvalidStateFileError(f"{context}: missing required metadata key '{key}'")
+ raise InvalidStateFileError(
+ f"{context}: missing required metadata key '{key}'"
+ )
# ``success`` is bool and must be checked separately to avoid the
# numeric-bool guard in ``_require_type``.
if expected is bool:
@@ -267,7 +273,9 @@ def _validate_app_state_schema(data: dict[str, Any]) -> None:
)
sub = _require_mapping(payload, f"results.{etype}")
if "arrays" not in sub or "metadata" not in sub:
- raise InvalidStateFileError(f"results.{etype}: must contain 'arrays' and 'metadata'")
+ raise InvalidStateFileError(
+ f"results.{etype}: must contain 'arrays' and 'metadata'"
+ )
_validate_arrays_block(sub["arrays"], f"results.{etype}")
_validate_metadata_block(sub["metadata"], f"results.{etype}")
@@ -398,7 +406,9 @@ def save_app_state(
slider_values maps slider_name -> float value.
"""
state_path = (
- load_app_paths().state_file if state_dir is None else Path(state_dir) / "last_state.json"
+ load_app_paths().state_file
+ if state_dir is None
+ else Path(state_dir) / "last_state.json"
)
state_path.parent.mkdir(parents=True, exist_ok=True)
@@ -426,7 +436,9 @@ def load_app_state(*, state_dir: str | Path | None = None) -> dict[str, Any] | N
state is incompatible rather than being silently discarded.
"""
state_path = (
- load_app_paths().state_file if state_dir is None else Path(state_dir) / "last_state.json"
+ load_app_paths().state_file
+ if state_dir is None
+ else Path(state_dir) / "last_state.json"
)
if not state_path.exists():
diff --git a/src/movement_optimizer/rendering.py b/src/movement_optimizer/rendering.py
index 54e752e25c..24904fda96 100644
--- a/src/movement_optimizer/rendering.py
+++ b/src/movement_optimizer/rendering.py
@@ -232,7 +232,9 @@ def draw_ghost(
HEAD_RADIUS = 0.10 # metres
@classmethod
- def draw_segments(cls, ax: Axes, joints: dict[str, NDArray], body_height: float = 1.75) -> None:
+ def draw_segments(
+ cls, ax: Axes, joints: dict[str, NDArray], body_height: float = 1.75
+ ) -> None:
pts = [joints["ankle"], joints["knee"], joints["hip"], joints["shoulder"]]
for k in range(3):
ax.plot(
diff --git a/src/movement_optimizer/result_analysis.py b/src/movement_optimizer/result_analysis.py
index da3d032b99..07eec3c1fd 100644
--- a/src/movement_optimizer/result_analysis.py
+++ b/src/movement_optimizer/result_analysis.py
@@ -79,7 +79,9 @@ def recommendations(self) -> list[str]:
"""Return result-driven recommendations for the exported report."""
recommendations: list[str] = []
if not self.result.success:
- recommendations.append("Review optimization settings; the solver did not converge.")
+ recommendations.append(
+ "Review optimization settings; the solver did not converge."
+ )
if self.result.n_joint_limit_violations > 0:
recommendations.append(
"Review joint limits; the trajectory exceeded configured bounds."
@@ -95,10 +97,14 @@ def recommendations(self) -> list[str]:
]
if high_torque_joints:
joined = ", ".join(high_torque_joints)
- recommendations.append(f"Review load selection; peak torque is high at: {joined}.")
+ recommendations.append(
+ f"Review load selection; peak torque is high at: {joined}."
+ )
if not recommendations:
- recommendations.append("No immediate issues detected in the exported result.")
+ recommendations.append(
+ "No immediate issues detected in the exported result."
+ )
return recommendations
def com_range_cm(self) -> float:
diff --git a/src/movement_optimizer/strength.py b/src/movement_optimizer/strength.py
index 54ba53e4fc..cc120407af 100644
--- a/src/movement_optimizer/strength.py
+++ b/src/movement_optimizer/strength.py
@@ -66,7 +66,9 @@ def torque_angle_factor(self, q: float | NDArray) -> NDArray:
raise ValueError("q must not contain NaN values")
return np.exp(-(((q_arr - self.q_optimal) / self.angle_width) ** 2))
- def torque_velocity_factor(self, qd: float | NDArray, torque_sign: float = -1.0) -> NDArray:
+ def torque_velocity_factor(
+ self, qd: float | NDArray, torque_sign: float = -1.0
+ ) -> NDArray:
"""Hill-type force-velocity scaling factor.
Branch selection is based on whether the muscle is shortening
@@ -97,7 +99,9 @@ def torque_velocity_factor(self, qd: float | NDArray, torque_sign: float = -1.0)
def available_torque(self, q: float | NDArray, qd: float | NDArray) -> NDArray:
"""Maximum torque the joint can produce at given angle and velocity."""
- return self.tau_max * self.torque_angle_factor(q) * self.torque_velocity_factor(qd)
+ return (
+ self.tau_max * self.torque_angle_factor(q) * self.torque_velocity_factor(qd)
+ )
class JointTorqueSet:
@@ -152,10 +156,14 @@ def available_torques_batch(self, q: NDArray, qd: NDArray) -> NDArray:
"""Compute available torque at each joint for N poses."""
result = np.empty((q.shape[0], len(self.joint_names)))
for index, name in enumerate(self.joint_names):
- result[:, index] = self._models[name].available_torque(q[:, index], qd[:, index])
+ result[:, index] = self._models[name].available_torque(
+ q[:, index], qd[:, index]
+ )
return result
- def torque_utilization(self, q: NDArray, qd: NDArray, required_torques: NDArray) -> NDArray:
+ def torque_utilization(
+ self, q: NDArray, qd: NDArray, required_torques: NDArray
+ ) -> NDArray:
"""Ratio of required torque to available torque."""
available = self.available_torques_batch(q, qd)
safe_available = np.maximum(available, 1e-10)
diff --git a/src/movement_optimizer/tests/test_anim_renderer.py b/src/movement_optimizer/tests/test_anim_renderer.py
index 763d9723fc..1a62487761 100644
--- a/src/movement_optimizer/tests/test_anim_renderer.py
+++ b/src/movement_optimizer/tests/test_anim_renderer.py
@@ -91,7 +91,9 @@ def test_draw_anim_frame_deadlift(self, mock_ax, mock_dynamics, dummy_result, bo
mock_ax.clear.assert_called_once()
mock_ax.set_title.assert_called_once()
- def test_draw_anim_frame_bench_press(self, mock_ax, mock_dynamics, dummy_result, body):
+ def test_draw_anim_frame_bench_press(
+ self, mock_ax, mock_dynamics, dummy_result, body
+ ):
draw_anim_frame(
mock_ax,
5,
diff --git a/src/movement_optimizer/tests/test_bench_press.py b/src/movement_optimizer/tests/test_bench_press.py
index 05b09d31d3..944e0e8065 100644
--- a/src/movement_optimizer/tests/test_bench_press.py
+++ b/src/movement_optimizer/tests/test_bench_press.py
@@ -78,17 +78,17 @@ def test_bench_start_is_lockout(self, default_body: BodyModel) -> None:
"""q_start should have shoulder near 0 degrees (arms vertical/lockout)."""
_dyn, qs, _qe, _qb, _q_via = make_bench_press_config(default_body, 60.0)
shoulder_deg = np.degrees(qs[0])
- assert abs(shoulder_deg) < 5, (
- f"At lockout shoulder should be near 0 deg, got {shoulder_deg:.1f}"
- )
+ assert (
+ abs(shoulder_deg) < 5
+ ), f"At lockout shoulder should be near 0 deg, got {shoulder_deg:.1f}"
def test_bench_via_is_chest(self, default_body: BodyModel) -> None:
"""q_via should have shoulder near 80 degrees (upper arm horizontal)."""
_dyn, _qs, _qe, _qb, q_via = make_bench_press_config(default_body, 60.0)
shoulder_deg = np.degrees(q_via[0])
- assert 70 < shoulder_deg < 95, (
- f"At chest touch shoulder should be ~80 deg, got {shoulder_deg:.1f}"
- )
+ assert (
+ 70 < shoulder_deg < 95
+ ), f"At chest touch shoulder should be ~80 deg, got {shoulder_deg:.1f}"
def test_bench_full_rep(self, default_body: BodyModel) -> None:
"""q_start should equal q_end (full rep returns to lockout)."""
diff --git a/src/movement_optimizer/tests/test_benchmarks.py b/src/movement_optimizer/tests/test_benchmarks.py
index e5228e7105..ffa80171e3 100644
--- a/src/movement_optimizer/tests/test_benchmarks.py
+++ b/src/movement_optimizer/tests/test_benchmarks.py
@@ -73,8 +73,12 @@ def test_single_inverse_dynamics_speed(self, default_body: BodyModel):
for _ in range(10):
dyn.inverse_dynamics(q, qd, qdd)
- per_call_ms = _measure_ms(lambda: dyn.inverse_dynamics(q, qd, qdd), iterations=1000)
- assert per_call_ms < 2.0, f"Single ID call took {per_call_ms:.3f}ms median (limit: 2ms)"
+ per_call_ms = _measure_ms(
+ lambda: dyn.inverse_dynamics(q, qd, qdd), iterations=1000
+ )
+ assert (
+ per_call_ms < 2.0
+ ), f"Single ID call took {per_call_ms:.3f}ms median (limit: 2ms)"
def test_batch_inverse_dynamics_speed(self, default_body: BodyModel):
"""Batch inverse dynamics (100 timesteps) should complete in < 50ms (median).
@@ -94,8 +98,12 @@ def test_batch_inverse_dynamics_speed(self, default_body: BodyModel):
for _ in range(5):
dyn.inverse_dynamics_batch(q, qd, qdd)
- per_call_ms = _measure_ms(lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=100)
- assert per_call_ms < 50.0, f"Batch ID (N=100) took {per_call_ms:.3f}ms median (limit: 50ms)"
+ per_call_ms = _measure_ms(
+ lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=100
+ )
+ assert (
+ per_call_ms < 50.0
+ ), f"Batch ID (N=100) took {per_call_ms:.3f}ms median (limit: 50ms)"
class TestMassMatrixBenchmark:
@@ -109,7 +117,9 @@ def test_mass_matrix_speed(self, default_body: BodyModel):
dyn.mass_matrix(q)
per_call_ms = _measure_ms(lambda: dyn.mass_matrix(q), iterations=1000)
- assert per_call_ms < 1.0, f"Mass matrix took {per_call_ms:.3f}ms median (limit: 1ms)"
+ assert (
+ per_call_ms < 1.0
+ ), f"Mass matrix took {per_call_ms:.3f}ms median (limit: 1ms)"
class TestForwardKinematicsBenchmark:
@@ -134,7 +144,9 @@ def test_body_model_construction_speed(self):
BodyModel(75.0, 1.75)
per_call_ms = _measure_ms(lambda: BodyModel(75.0, 1.75), iterations=1000)
- assert per_call_ms < 2.0, f"BodyModel init took {per_call_ms:.3f}ms median (limit: 2ms)"
+ assert (
+ per_call_ms < 2.0
+ ), f"BodyModel init took {per_call_ms:.3f}ms median (limit: 2ms)"
# ===========================================================================
@@ -207,9 +219,13 @@ def test_batch_id_typical_grid_under_budget(self, default_body: BodyModel):
for _ in range(5):
dyn.inverse_dynamics_batch(q, qd, qdd)
- per_call_ms = _measure_ms(lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=200)
+ per_call_ms = _measure_ms(
+ lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=200
+ )
logger.info("batch ID (N=%d) median %.4f ms", n, per_call_ms)
- assert per_call_ms < 25.0, f"Batch ID (N={n}) took {per_call_ms:.3f}ms median (limit: 25ms)"
+ assert (
+ per_call_ms < 25.0
+ ), f"Batch ID (N={n}) took {per_call_ms:.3f}ms median (limit: 25ms)"
def test_batch_id_scales_subquadratic(self, default_body: BodyModel):
"""Doubling N should not multiply batch-ID time by more than 4x.
@@ -226,7 +242,9 @@ def time_batch(n: int) -> float:
qdd = rng.uniform(-5.0, 5.0, (n, 3))
for _ in range(5):
dyn.inverse_dynamics_batch(q, qd, qdd)
- return _measure_ms(lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=200)
+ return _measure_ms(
+ lambda: dyn.inverse_dynamics_batch(q, qd, qdd), iterations=200
+ )
t_small = time_batch(50)
t_large = time_batch(200)
@@ -257,7 +275,9 @@ def test_compute_cost_under_budget(self, default_body: BodyModel):
per_call_ms = _measure_ms(lambda: opt._compute_cost(x0), iterations=200)
logger.info("_compute_cost (n_eval=20) median %.4f ms", per_call_ms)
- assert per_call_ms < 5.0, f"_compute_cost took {per_call_ms:.3f}ms median (limit: 5ms)"
+ assert (
+ per_call_ms < 5.0
+ ), f"_compute_cost took {per_call_ms:.3f}ms median (limit: 5ms)"
class TestEndToEndOptimizer:
@@ -274,7 +294,9 @@ def test_small_problem_under_10s(self, default_body: BodyModel):
opt = _make_squat_optimizer(default_body, n_eval=20, n_starts=2)
elapsed = _best_of(lambda: opt.optimize(), trials=2)
logger.info("end-to-end optimize (n_eval=20, n_starts=2) %.3f s", elapsed)
- assert elapsed < 10.0, f"Optimizer took {elapsed:.2f}s for a small problem (limit: 10s)"
+ assert (
+ elapsed < 10.0
+ ), f"Optimizer took {elapsed:.2f}s for a small problem (limit: 10s)"
class TestOptimizerScaling:
@@ -303,7 +325,9 @@ def run_at(n_eval: int) -> float:
t_small = run_at(10)
t_large = run_at(20)
- logger.info("optimizer scaling: n_eval=10 %.3fs, n_eval=20 %.3fs", t_small, t_large)
+ logger.info(
+ "optimizer scaling: n_eval=10 %.3fs, n_eval=20 %.3fs", t_small, t_large
+ )
# Floor plus absolute cap prevent division blow-up when both runs are
# very fast (sub-second) and scheduler noise dominates the ratio.
baseline = max(t_small, 0.05)
@@ -346,7 +370,9 @@ def test_cache_hit_much_faster_than_miss(self, default_body: BodyModel):
)
per_hit_s = per_hit_ms / 1000.0
ratio = t_miss / per_hit_s if per_hit_s > 0 else float("inf")
- logger.info("cache miss %.4fs vs hit %.6fs (ratio %.0fx)", t_miss, per_hit_s, ratio)
+ logger.info(
+ "cache miss %.4fs vs hit %.6fs (ratio %.0fx)", t_miss, per_hit_s, ratio
+ )
# Absolute upper bound on a single hit lookup so we catch the case
# where a hit becomes unexpectedly expensive (e.g. deep copy added
diff --git a/src/movement_optimizer/tests/test_bilateral_3d.py b/src/movement_optimizer/tests/test_bilateral_3d.py
index 6c6be4aec5..20332f462d 100644
--- a/src/movement_optimizer/tests/test_bilateral_3d.py
+++ b/src/movement_optimizer/tests/test_bilateral_3d.py
@@ -63,7 +63,9 @@ def test_t_pose_ankles_on_ground(self, model: Bilateral3DModel) -> None:
assert fk["left_ankle"][2] == pytest.approx(0.0)
assert fk["right_ankle"][2] == pytest.approx(0.0)
- def test_t_pose_shoulder_height_equals_sum_of_segments(self, model: Bilateral3DModel) -> None:
+ def test_t_pose_shoulder_height_equals_sum_of_segments(
+ self, model: Bilateral3DModel
+ ) -> None:
fk = model.forward_kinematics(model.t_pose())
expected_height = model.L_shin + model.L_thigh + model.L_torso
assert fk["shoulder"][2] == pytest.approx(expected_height)
@@ -85,7 +87,9 @@ def test_t_pose_pelvis_midway(self, model: Bilateral3DModel) -> None:
class TestKneeFlexion:
"""Flexing only the knee should produce a known-position check."""
- def test_90deg_knee_flex_drops_hip_by_thigh_length(self, model: Bilateral3DModel) -> None:
+ def test_90deg_knee_flex_drops_hip_by_thigh_length(
+ self, model: Bilateral3DModel
+ ) -> None:
# Flex the left knee 90deg forward: ankle stays, shin stays vertical,
# thigh now horizontal (pointing +x). So left_hip should be at
# (L_thigh, +half_w, L_shin) -- the thigh rotated from "up" to "forward".
@@ -101,7 +105,9 @@ def test_90deg_knee_flex_drops_hip_by_thigh_length(self, model: Bilateral3DModel
np.testing.assert_allclose(fk["left_hip"], expected, atol=1e-10)
# Right hip untouched
- expected_right = np.array([0.0, -0.5 * model.stance_width_m, model.L_shin + model.L_thigh])
+ expected_right = np.array(
+ [0.0, -0.5 * model.stance_width_m, model.L_shin + model.L_thigh]
+ )
np.testing.assert_allclose(fk["right_hip"], expected_right, atol=1e-10)
@@ -142,13 +148,17 @@ def xz(p3: np.ndarray) -> np.ndarray:
class TestInputValidation:
- def test_forward_kinematics_rejects_raw_tuple(self, model: Bilateral3DModel) -> None:
+ def test_forward_kinematics_rejects_raw_tuple(
+ self, model: Bilateral3DModel
+ ) -> None:
with pytest.raises(TypeError, match="Bilateral3DPose"):
model.forward_kinematics((0.0, 0.0, 0.0)) # type: ignore[arg-type]
class TestSegmentPairs:
- def test_segment_pairs_reference_valid_joints(self, model: Bilateral3DModel) -> None:
+ def test_segment_pairs_reference_valid_joints(
+ self, model: Bilateral3DModel
+ ) -> None:
fk = model.forward_kinematics(model.t_pose())
for a, b in model.segment_pairs():
assert a in fk, f"unknown joint {a}"
diff --git a/src/movement_optimizer/tests/test_chain_forces.py b/src/movement_optimizer/tests/test_chain_forces.py
index 13eeacd87d..cdaed04cf4 100644
--- a/src/movement_optimizer/tests/test_chain_forces.py
+++ b/src/movement_optimizer/tests/test_chain_forces.py
@@ -82,7 +82,12 @@ def test_chain_force_field_shapes_and_gravity() -> None:
config, rollout = _make_rollout()
field = chain_force_field(config, rollout, _DT, frame_index=2)
assert isinstance(field, ChainForceField)
- for array in (field.midpoints_m, field.gravity_n, field.tension_n, field.net_force_n):
+ for array in (
+ field.midpoints_m,
+ field.gravity_n,
+ field.tension_n,
+ field.net_force_n,
+ ):
assert array.shape == (_SEGMENTS, 2)
assert np.all(np.isfinite(array))
expected = config.link_mass_kg * config.gravity_m_s2
diff --git a/src/movement_optimizer/tests/test_cli.py b/src/movement_optimizer/tests/test_cli.py
index f28bf66eb1..802601b5a6 100644
--- a/src/movement_optimizer/tests/test_cli.py
+++ b/src/movement_optimizer/tests/test_cli.py
@@ -139,7 +139,9 @@ def optimize(self):
class TestMain:
- def test_main_writes_output_file(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
+ def test_main_writes_output_file(
+ self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+ ):
result = make_test_result(cost=12.3)
_FakeOptimizer.init_calls = []
_FakeOptimizer.next_result = result
@@ -150,7 +152,9 @@ def test_main_writes_output_file(self, monkeypatch: pytest.MonkeyPatch, tmp_path
np.ones(3),
np.zeros((3, 2)),
)
- monkeypatch.setitem(cli.EXERCISE_FACTORIES, "squat", lambda body, bar_mass: fake_config)
+ monkeypatch.setitem(
+ cli.EXERCISE_FACTORIES, "squat", lambda body, bar_mass: fake_config
+ )
monkeypatch.setattr(cli, "TrajectoryOptimizer", _FakeOptimizer)
output_path = tmp_path / "result.json"
@@ -164,7 +168,9 @@ def test_main_writes_output_file(self, monkeypatch: pytest.MonkeyPatch, tmp_path
assert _FakeOptimizer.init_calls[-1]["kwargs"]["duration"] == 2.0
assert _FakeOptimizer.init_calls[-1]["kwargs"]["q_via"] is None
- def test_main_emits_summary_for_multiphase_lift(self, monkeypatch: pytest.MonkeyPatch):
+ def test_main_emits_summary_for_multiphase_lift(
+ self, monkeypatch: pytest.MonkeyPatch
+ ):
result = make_test_result(cost=7.5)
result.success = False
_FakeOptimizer.init_calls = []
@@ -180,9 +186,13 @@ def test_main_emits_summary_for_multiphase_lift(self, monkeypatch: pytest.Monkey
)
emitted: list[dict[str, Any]] = []
- monkeypatch.setitem(cli.EXERCISE_FACTORIES, "clean", lambda body, bar_mass: fake_config)
+ monkeypatch.setitem(
+ cli.EXERCISE_FACTORIES, "clean", lambda body, bar_mass: fake_config
+ )
monkeypatch.setattr(cli, "TrajectoryOptimizer", _FakeOptimizer)
- monkeypatch.setattr(cli, "_emit_cli_summary", lambda summary: emitted.append(summary))
+ monkeypatch.setattr(
+ cli, "_emit_cli_summary", lambda summary: emitted.append(summary)
+ )
exit_code = cli.main(["--exercise", "clean", "--duration", "1.0", "--verbose"])
diff --git a/src/movement_optimizer/tests/test_edge_cases.py b/src/movement_optimizer/tests/test_edge_cases.py
index 3ececa19ad..e6d8794ec6 100644
--- a/src/movement_optimizer/tests/test_edge_cases.py
+++ b/src/movement_optimizer/tests/test_edge_cases.py
@@ -92,12 +92,12 @@ def _assert_result_finite(result: OptimizationResult, n_eval: int) -> None:
def _assert_inner_bos(result: OptimizationResult, body: BodyModel) -> None:
"""COM must respect the inner-BOS hard constraint (with loose slack)."""
com_x = result.com[:, 0]
- assert np.all(com_x >= body.inner_heel - _BOS_TOL_M), (
- f"COM below inner_heel: min={com_x.min():.4f}, bound={body.inner_heel:.4f}"
- )
- assert np.all(com_x <= body.inner_toe + _BOS_TOL_M), (
- f"COM above inner_toe: max={com_x.max():.4f}, bound={body.inner_toe:.4f}"
- )
+ assert np.all(
+ com_x >= body.inner_heel - _BOS_TOL_M
+ ), f"COM below inner_heel: min={com_x.min():.4f}, bound={body.inner_heel:.4f}"
+ assert np.all(
+ com_x <= body.inner_toe + _BOS_TOL_M
+ ), f"COM above inner_toe: max={com_x.max():.4f}, bound={body.inner_toe:.4f}"
# ---------------------------------------------------------------------------
@@ -248,9 +248,9 @@ def test_identical_start_and_end_angles(self) -> None:
_assert_result_finite(result, opt.n_eval)
# Each joint should travel less than ~3 degrees from the constant pose.
max_travel_rad = float(np.max(np.abs(result.q - qs)))
- assert max_travel_rad < np.radians(15.0), (
- f"Zero-ROM trajectory drifted {np.degrees(max_travel_rad):.2f} deg"
- )
+ assert max_travel_rad < np.radians(
+ 15.0
+ ), f"Zero-ROM trajectory drifted {np.degrees(max_travel_rad):.2f} deg"
# ---------------------------------------------------------------------------
@@ -267,7 +267,9 @@ def test_single_start(self) -> None:
_assert_result_finite(result, opt.n_eval)
assert result.success
- @pytest.mark.xfail(reason="SLSQP multistart convergence is unstable on some platforms")
+ @pytest.mark.xfail(
+ reason="SLSQP multistart convergence is unstable on some platforms"
+ )
def test_many_multistarts(self) -> None:
"""A larger n_starts exercises the parallel path and must succeed."""
body = BodyModel(75.0, 1.75)
@@ -335,7 +337,16 @@ def test_too_few_waypoints_raises(self) -> None:
dyn, qs, qe, qb = make_squat_config(body, 60.0)
with pytest.raises(ValueError, match=r">= 4 waypoints"):
TrajectoryOptimizer(
- body, dyn, "squat", 60.0, qs, qe, qb, n_waypoints=3, n_eval=20, n_starts=1
+ body,
+ dyn,
+ "squat",
+ 60.0,
+ qs,
+ qe,
+ qb,
+ n_waypoints=3,
+ n_eval=20,
+ n_starts=1,
)
def test_minimum_waypoints_accepted(self) -> None:
diff --git a/src/movement_optimizer/tests/test_exercise_tab.py b/src/movement_optimizer/tests/test_exercise_tab.py
index 1416f607ea..07fc426e4a 100644
--- a/src/movement_optimizer/tests/test_exercise_tab.py
+++ b/src/movement_optimizer/tests/test_exercise_tab.py
@@ -239,7 +239,9 @@ def test_draw_anim_frame_passes_tab_name(self, mock_anim_renderer) -> None:
assert "Deadlift" in call_args
@patch("movement_optimizer.gui.exercise_tab.anim_renderer")
- def test_draw_anim_frame_passes_correct_frame_index(self, mock_anim_renderer) -> None:
+ def test_draw_anim_frame_passes_correct_frame_index(
+ self, mock_anim_renderer
+ ) -> None:
from movement_optimizer.gui.exercise_tab import ExerciseTab
tab = ExerciseTab("Squat")
diff --git a/src/movement_optimizer/tests/test_exercises.py b/src/movement_optimizer/tests/test_exercises.py
index 864f1ea5b7..6a0842d0ca 100644
--- a/src/movement_optimizer/tests/test_exercises.py
+++ b/src/movement_optimizer/tests/test_exercises.py
@@ -85,7 +85,9 @@ def test_jerk_start_at_rack(self, default_body: BodyModel) -> None:
def test_jerk_end_overhead(self, default_body: BodyModel) -> None:
dyn, _qs, qe, _qb, _q_via = make_jerk_config(default_body, 60.0)
# End: torso near vertical (bar overhead)
- assert abs(qe[2]) < np.radians(10), "Jerk end: torso must be near vertical (overhead)"
+ assert abs(qe[2]) < np.radians(
+ 10
+ ), "Jerk end: torso must be near vertical (overhead)"
# Shoulder should be near standing height (overhead lockout)
fk = dyn.forward_kinematics(qe)
shoulder_h = fk["shoulder"][1]
@@ -115,11 +117,15 @@ def test_snatch_start_near_floor(self, default_body: BodyModel) -> None:
def test_snatch_end_overhead(self, default_body: BodyModel) -> None:
dyn, _qs, qe, _qb, _q_via = make_snatch_config(default_body, 60.0)
# End: torso near vertical (bar overhead)
- assert abs(qe[2]) < np.radians(10), "Snatch end: torso must be near vertical (overhead)"
+ assert abs(qe[2]) < np.radians(
+ 10
+ ), "Snatch end: torso must be near vertical (overhead)"
fk = dyn.forward_kinematics(qe)
shoulder_h = fk["shoulder"][1]
total_h = default_body.L.sum()
- assert shoulder_h > total_h * 0.90, "Snatch end: shoulder must be high (overhead)"
+ assert (
+ shoulder_h > total_h * 0.90
+ ), "Snatch end: shoulder must be high (overhead)"
def test_snatch_has_via_points(self, default_body: BodyModel) -> None:
_dyn, _qs, _qe, _qb, q_via = make_snatch_config(default_body, 60.0)
@@ -131,7 +137,9 @@ def test_snatch_via_is_overhead_squat(self, default_body: BodyModel) -> None:
assert q_via[1] < np.radians(-60), "Snatch via: should be deep squat"
# Torso relatively upright for overhead position
# balance_pose may adjust the torso angle to maintain COM balance
- assert abs(q_via[2]) < np.radians(80), "Snatch via: torso should be reasonably upright"
+ assert abs(q_via[2]) < np.radians(
+ 80
+ ), "Snatch via: torso should be reasonably upright"
# ------------------------------------------------------------------
@@ -179,7 +187,9 @@ def test_bench_no_com_constraint(self, default_body: BodyModel) -> None:
n_waypoints=8,
)
constraints = opt._build_constraints()
- assert len(constraints) == 1, "Bench press should keep only the joint-limit constraint"
+ assert (
+ len(constraints) == 1
+ ), "Bench press should keep only the joint-limit constraint"
assert constraints[0]["fun"] is joint_limit_constraint_values
@@ -208,8 +218,12 @@ def _check_com_in_inner_bos(
def test_clean_endpoints_balanced(self, default_body: BodyModel) -> None:
dyn, qs, qe, _qb, _q_via = make_clean_config(default_body, 60.0)
- self._check_com_in_inner_bos(default_body, dyn, qs, "deadlift", 60.0, "clean start")
- self._check_com_in_inner_bos(default_body, dyn, qe, "deadlift", 60.0, "clean end")
+ self._check_com_in_inner_bos(
+ default_body, dyn, qs, "deadlift", 60.0, "clean start"
+ )
+ self._check_com_in_inner_bos(
+ default_body, dyn, qe, "deadlift", 60.0, "clean end"
+ )
def test_jerk_endpoints_balanced(self, default_body: BodyModel) -> None:
dyn, qs, qe, _qb, _q_via = make_jerk_config(default_body, 60.0)
@@ -218,5 +232,7 @@ def test_jerk_endpoints_balanced(self, default_body: BodyModel) -> None:
def test_snatch_endpoints_balanced(self, default_body: BodyModel) -> None:
dyn, qs, qe, _qb, _q_via = make_snatch_config(default_body, 60.0)
- self._check_com_in_inner_bos(default_body, dyn, qs, "deadlift", 60.0, "snatch start")
+ self._check_com_in_inner_bos(
+ default_body, dyn, qs, "deadlift", 60.0, "snatch start"
+ )
self._check_com_in_inner_bos(default_body, dyn, qe, "squat", 60.0, "snatch end")
diff --git a/src/movement_optimizer/tests/test_export.py b/src/movement_optimizer/tests/test_export.py
index f3869d3793..00b44d35c6 100644
--- a/src/movement_optimizer/tests/test_export.py
+++ b/src/movement_optimizer/tests/test_export.py
@@ -257,7 +257,9 @@ def test_summary_contains_torque_statistics(self, tmp_path):
export_to_excel(r, str(path))
wb = openpyxl.load_workbook(str(path))
ws = wb["Summary"]
- all_values = [str(cell.value) for row in ws.iter_rows() for cell in row if cell.value]
+ all_values = [
+ str(cell.value) for row in ws.iter_rows() for cell in row if cell.value
+ ]
assert any("Peak" in v for v in all_values)
def test_statistics_sheet_contains_recommendations(self, tmp_path):
@@ -270,7 +272,9 @@ def test_statistics_sheet_contains_recommendations(self, tmp_path):
export_to_excel(r, str(path))
wb = openpyxl.load_workbook(str(path))
ws = wb["Statistics"]
- all_values = [str(cell.value) for row in ws.iter_rows() for cell in row if cell.value]
+ all_values = [
+ str(cell.value) for row in ws.iter_rows() for cell in row if cell.value
+ ]
assert "Recommendations" in all_values
def test_raises_on_none_result(self, tmp_path):
diff --git a/src/movement_optimizer/tests/test_export_excel.py b/src/movement_optimizer/tests/test_export_excel.py
index 6f078ecf9e..64c5d9eb8b 100644
--- a/src/movement_optimizer/tests/test_export_excel.py
+++ b/src/movement_optimizer/tests/test_export_excel.py
@@ -36,7 +36,9 @@ def test_summary_sheet_has_non_empty_data(self, tmp_path):
wb = openpyxl.load_workbook(str(path))
ws = wb["Summary"]
non_empty_rows = [
- row for row in ws.iter_rows(values_only=True) if any(v is not None for v in row)
+ row
+ for row in ws.iter_rows(values_only=True)
+ if any(v is not None for v in row)
]
assert len(non_empty_rows) > 0
@@ -92,7 +94,11 @@ def test_optional_metadata_written_to_summary(self, tmp_path):
path = tmp_path / "meta.xlsx"
export_to_excel(
- result, path, exercise_name="Deadlift", body_mass_kg=80.0, body_height_m=1.82
+ result,
+ path,
+ exercise_name="Deadlift",
+ body_mass_kg=80.0,
+ body_height_m=1.82,
)
wb = openpyxl.load_workbook(str(path))
@@ -111,7 +117,9 @@ def test_statistics_sheet_contains_required_metrics(self, tmp_path):
wb = openpyxl.load_workbook(str(path))
ws = wb["Statistics"]
- values = [cell for row in ws.iter_rows(values_only=True) for cell in row if cell]
+ values = [
+ cell for row in ws.iter_rows(values_only=True) for cell in row if cell
+ ]
assert "Mean (N*m)" in values
assert "Std dev (N*m)" in values
assert "Min (N*m)" in values
diff --git a/src/movement_optimizer/tests/test_gait_sts.py b/src/movement_optimizer/tests/test_gait_sts.py
index 80ac6e244c..80706f0e6b 100644
--- a/src/movement_optimizer/tests/test_gait_sts.py
+++ b/src/movement_optimizer/tests/test_gait_sts.py
@@ -73,7 +73,9 @@ def test_spatiotemporal_basic(self, default_body: BodyModel) -> None:
assert result["walking_speed_m_s"] == pytest.approx(0.7, rel=1e-6)
assert result["cycle_duration_s"] == pytest.approx(1.0, rel=1e-6)
assert 0.0 < result["stance_phase_pct"] < 100.0
- assert result["stance_phase_pct"] + result["swing_phase_pct"] == pytest.approx(100.0)
+ assert result["stance_phase_pct"] + result["swing_phase_pct"] == pytest.approx(
+ 100.0
+ )
def test_symmetry_index_identical(self, default_body: BodyModel) -> None:
analyzer = GaitAnalyzer(default_body)
diff --git a/src/movement_optimizer/tests/test_help_dialog.py b/src/movement_optimizer/tests/test_help_dialog.py
index c96cde95c7..c86ecbd842 100644
--- a/src/movement_optimizer/tests/test_help_dialog.py
+++ b/src/movement_optimizer/tests/test_help_dialog.py
@@ -16,9 +16,13 @@ def test_help_center_exposes_required_offline_topics(qapp) -> None:
assert len(HELP_TOPICS) >= 5
assert dialog.tabs.count() >= 5
- assert {"getting_started", "parameters", "results", "troubleshooting", "glossary"} <= set(
- HELP_TOPICS
- )
+ assert {
+ "getting_started",
+ "parameters",
+ "results",
+ "troubleshooting",
+ "glossary",
+ } <= set(HELP_TOPICS)
def test_help_center_can_select_each_topic(qapp) -> None:
@@ -34,10 +38,15 @@ def test_help_center_contains_glossary_terms(qapp) -> None:
assert len(GLOSSARY) >= 7
assert {"COM", "BOS", "Torque", "ROM"} <= set(GLOSSARY)
- assert dialog.tabs.tabText(dialog.tabs.currentIndex()) == HELP_TOPICS["glossary"].title
+ assert (
+ dialog.tabs.tabText(dialog.tabs.currentIndex()) == HELP_TOPICS["glossary"].title
+ )
def test_parameter_help_dialog_opens_parameter_topic(qapp) -> None:
dialog = ParameterHelpDialog()
- assert dialog.tabs.tabText(dialog.tabs.currentIndex()) == HELP_TOPICS["parameters"].title
+ assert (
+ dialog.tabs.tabText(dialog.tabs.currentIndex())
+ == HELP_TOPICS["parameters"].title
+ )
diff --git a/src/movement_optimizer/tests/test_hypothesis.py b/src/movement_optimizer/tests/test_hypothesis.py
index 166cac0cbd..a71413bfdc 100644
--- a/src/movement_optimizer/tests/test_hypothesis.py
+++ b/src/movement_optimizer/tests/test_hypothesis.py
@@ -24,7 +24,9 @@
build_splines,
eval_trajectory,
)
-from movement_optimizer.trajectory.optimizer_constraints import joint_limit_constraint_values
+from movement_optimizer.trajectory.optimizer_constraints import (
+ joint_limit_constraint_values,
+)
from movement_optimizer.trajectory.optimizer_cost import (
compute_torque_cost,
compute_torque_rate_cost,
@@ -86,7 +88,9 @@ def test_body_model_rejects_nonpositive_mass(self, body_mass: float):
to=st.floats(min_value=0.5, max_value=2.0),
)
@settings(max_examples=100)
- def test_segment_multipliers_preserve_proportionality(self, ll: float, ul: float, to: float):
+ def test_segment_multipliers_preserve_proportionality(
+ self, ll: float, ul: float, to: float
+ ):
"""Segment lengths should scale linearly with multipliers."""
base = BodyModel(75.0, 1.75)
scaled = BodyModel(
@@ -222,7 +226,9 @@ def test_constant_in_bounds_spline_satisfies_joint_constraints(
def build_splines_fn(flat_x: np.ndarray):
return build_splines(flat_x, q, q, None, t_ctrl, n_waypoints, 3)
- constraints = joint_limit_constraint_values(x, build_splines_fn, t_eval, q_bounds)
+ constraints = joint_limit_constraint_values(
+ x, build_splines_fn, t_eval, q_bounds
+ )
assert constraints.shape == (2 * len(t_eval) * 3,)
assert np.all(constraints >= -1e-10)
@@ -231,12 +237,18 @@ def build_splines_fn(flat_x: np.ndarray):
class TestOptimizationCostProperties:
@given(
values=st.lists(
- st.floats(min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False),
+ st.floats(
+ min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False
+ ),
min_size=6,
max_size=30,
),
- dt=st.floats(min_value=0.01, max_value=1.0, allow_nan=False, allow_infinity=False),
- scale=st.floats(min_value=0.0, max_value=5.0, allow_nan=False, allow_infinity=False),
+ dt=st.floats(
+ min_value=0.01, max_value=1.0, allow_nan=False, allow_infinity=False
+ ),
+ scale=st.floats(
+ min_value=0.0, max_value=5.0, allow_nan=False, allow_infinity=False
+ ),
)
@settings(max_examples=75)
def test_torque_cost_scales_quadratically(
@@ -252,17 +264,29 @@ def test_torque_cost_scales_quadratically(
base_cost = compute_torque_cost(torques, dt)
scaled_cost = compute_torque_cost(scale * torques, dt)
- np.testing.assert_allclose(scaled_cost, scale**2 * base_cost, rtol=1e-12, atol=1e-9)
+ np.testing.assert_allclose(
+ scaled_cost, scale**2 * base_cost, rtol=1e-12, atol=1e-9
+ )
@given(
row=st.tuples(
- st.floats(min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False),
- st.floats(min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False),
- st.floats(min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False),
+ st.floats(
+ min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False
+ ),
+ st.floats(
+ min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False
+ ),
+ st.floats(
+ min_value=-200.0, max_value=200.0, allow_nan=False, allow_infinity=False
+ ),
),
n_eval=st.integers(min_value=2, max_value=20),
- dt=st.floats(min_value=0.01, max_value=1.0, allow_nan=False, allow_infinity=False),
- weight=st.floats(min_value=0.0, max_value=10.0, allow_nan=False, allow_infinity=False),
+ dt=st.floats(
+ min_value=0.01, max_value=1.0, allow_nan=False, allow_infinity=False
+ ),
+ weight=st.floats(
+ min_value=0.0, max_value=10.0, allow_nan=False, allow_infinity=False
+ ),
)
@settings(max_examples=75)
def test_torque_rate_cost_zero_for_constant_torque(
@@ -320,7 +344,9 @@ class TestTrajectoryOptimizerProperties:
bar_mass=st.floats(min_value=0.0, max_value=200.0),
)
@settings(max_examples=50)
- def test_optimizer_produces_finite_cost(self, body_mass: float, height: float, bar_mass: float):
+ def test_optimizer_produces_finite_cost(
+ self, body_mass: float, height: float, bar_mass: float
+ ):
"""Optimizer should always produce a finite cost for valid inputs."""
from movement_optimizer.models.exercise_configs import make_squat_config
from movement_optimizer.trajectory import TrajectoryOptimizer
@@ -350,7 +376,9 @@ def test_optimizer_produces_finite_cost(self, body_mass: float, height: float, b
q2=st.floats(min_value=-1.0, max_value=1.0),
)
@settings(max_examples=50)
- def test_cost_at_start_equals_end_for_static_pose(self, q0: float, q1: float, q2: float):
+ def test_cost_at_start_equals_end_for_static_pose(
+ self, q0: float, q1: float, q2: float
+ ):
"""Cost should be consistent for static start/end poses."""
from movement_optimizer.models.exercise_configs import make_squat_config
from movement_optimizer.trajectory import TrajectoryOptimizer
diff --git a/src/movement_optimizer/tests/test_import.py b/src/movement_optimizer/tests/test_import.py
index ffcdad7138..c6470a10c4 100644
--- a/src/movement_optimizer/tests/test_import.py
+++ b/src/movement_optimizer/tests/test_import.py
@@ -62,7 +62,9 @@ def test_legacy_file_without_format_version_emits_warning(self, tmp_path, caplog
path = tmp_path / "legacy.json"
path.write_text(json.dumps(data), encoding="utf-8")
- with caplog.at_level(logging.WARNING, logger="movement_optimizer.import_results"):
+ with caplog.at_level(
+ logging.WARNING, logger="movement_optimizer.import_results"
+ ):
result = import_result_from_json(path)
assert result["cost"] == 99.0
diff --git a/src/movement_optimizer/tests/test_install_nightly_system_deps.py b/src/movement_optimizer/tests/test_install_nightly_system_deps.py
index 56663837d2..c862abf9f0 100644
--- a/src/movement_optimizer/tests/test_install_nightly_system_deps.py
+++ b/src/movement_optimizer/tests/test_install_nightly_system_deps.py
@@ -13,7 +13,9 @@ def _completed_process(
stdout: str = "",
stderr: str = "",
) -> subprocess.CompletedProcess[str]:
- return subprocess.CompletedProcess(command, returncode, stdout=stdout, stderr=stderr)
+ return subprocess.CompletedProcess(
+ command, returncode, stdout=stdout, stderr=stderr
+ )
def test_run_with_lock_retries_retries_until_dpkg_lock_clears() -> None:
diff --git a/src/movement_optimizer/tests/test_issue_217_decompose.py b/src/movement_optimizer/tests/test_issue_217_decompose.py
index 1c3ca99abe..c4bd21e455 100644
--- a/src/movement_optimizer/tests/test_issue_217_decompose.py
+++ b/src/movement_optimizer/tests/test_issue_217_decompose.py
@@ -178,7 +178,9 @@ def test_4tuple_unpacks_all_fields(self) -> None:
qe = np.array([0.4, 0.5, 0.6])
qb = np.zeros((3, 2))
dyn = object()
- out_dyn, out_qs, out_qe, out_qb, out_via = _unpack_exercise_config((dyn, qs, qe, qb))
+ out_dyn, out_qs, out_qe, out_qb, out_via = _unpack_exercise_config(
+ (dyn, qs, qe, qb)
+ )
assert out_dyn is dyn
assert np.array_equal(out_qs, qs)
assert np.array_equal(out_qe, qe)
diff --git a/src/movement_optimizer/tests/test_issue_222_decompose.py b/src/movement_optimizer/tests/test_issue_222_decompose.py
index 0435f421c6..cccdd7d0e0 100644
--- a/src/movement_optimizer/tests/test_issue_222_decompose.py
+++ b/src/movement_optimizer/tests/test_issue_222_decompose.py
@@ -145,7 +145,9 @@ def test_writes_json_to_file(self, tmp_path: Path) -> None:
assert written["exercise"] == "squat"
assert written["cost"] == pytest.approx(5.0)
- def test_emits_summary_when_no_output(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ def test_emits_summary_when_no_output(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
from conftest import make_test_result
result = make_test_result(cost=7.7)
@@ -204,7 +206,9 @@ def test_consistent_with_inverse_dynamics_single_timestep(self) -> None:
d01 = q[:, 0] - q[:, 1]
d02 = q[:, 0] - q[:, 2]
d12 = q[:, 1] - q[:, 2]
- tau_inertia = dyn._batch_inertia_torques(qdd, np.cos(d01), np.cos(d02), np.cos(d12))
+ tau_inertia = dyn._batch_inertia_torques(
+ qdd, np.cos(d01), np.cos(d02), np.cos(d12)
+ )
tau_gravity = dyn._batch_gravity_torques(q)
tau_total = tau_inertia + tau_gravity
tau_ref = dyn.inverse_dynamics_batch(q, qd, qdd)
diff --git a/src/movement_optimizer/tests/test_issue_247_split_optimizer.py b/src/movement_optimizer/tests/test_issue_247_split_optimizer.py
index 3c5085fcb7..20b5481e6c 100644
--- a/src/movement_optimizer/tests/test_issue_247_split_optimizer.py
+++ b/src/movement_optimizer/tests/test_issue_247_split_optimizer.py
@@ -12,10 +12,17 @@
import numpy as np
import pytest
-from movement_optimizer.models import BodyModel, make_bench_press_config, make_squat_config
+from movement_optimizer.models import (
+ BodyModel,
+ make_bench_press_config,
+ make_squat_config,
+)
from movement_optimizer.trajectory import TrajectoryOptimizer
from movement_optimizer.trajectory.optimizer_bench import compute_bench_bar_cost
-from movement_optimizer.trajectory.optimizer_spline import build_splines, eval_trajectory
+from movement_optimizer.trajectory.optimizer_spline import (
+ build_splines,
+ eval_trajectory,
+)
# ---------------------------------------------------------------------------
# Fixtures
@@ -105,12 +112,12 @@ def test_splines_satisfy_boundary_conditions(self, squat_spline_args) -> None:
q_start_eval = spline(t0)
q_end_eval = spline(tf)
for j in range(a["n_dof"]):
- assert abs(float(q_start_eval[j]) - a["q_start"][j]) < 1e-10, (
- f"DOF {j}: spline does not pass through q_start"
- )
- assert abs(float(q_end_eval[j]) - a["q_end"][j]) < 1e-10, (
- f"DOF {j}: spline does not pass through q_end"
- )
+ assert (
+ abs(float(q_start_eval[j]) - a["q_start"][j]) < 1e-10
+ ), f"DOF {j}: spline does not pass through q_start"
+ assert (
+ abs(float(q_end_eval[j]) - a["q_end"][j]) < 1e-10
+ ), f"DOF {j}: spline does not pass through q_end"
def test_splines_with_via_point(self) -> None:
"""Via-point variant must also honour boundary conditions."""
diff --git a/src/movement_optimizer/tests/test_joint_limits.py b/src/movement_optimizer/tests/test_joint_limits.py
index 5aea81d59c..9b76604792 100644
--- a/src/movement_optimizer/tests/test_joint_limits.py
+++ b/src/movement_optimizer/tests/test_joint_limits.py
@@ -115,7 +115,9 @@ def test_custom_limits(self) -> None:
def test_bench_press_limits(self) -> None:
"""Bench press joints should have their own limits."""
q = np.array([np.radians(100), 0.0, np.radians(20)])
- q_clamped = clamp_joint_angles(q, BENCH_PRESS_JOINT_LIMITS, BENCH_PRESS_JOINT_NAMES)
+ q_clamped = clamp_joint_angles(
+ q, BENCH_PRESS_JOINT_LIMITS, BENCH_PRESS_JOINT_NAMES
+ )
for i, name in enumerate(BENCH_PRESS_JOINT_NAMES):
lo, hi = BENCH_PRESS_JOINT_LIMITS[name]
assert lo - 1e-10 <= q_clamped[i] <= hi + 1e-10
@@ -296,7 +298,9 @@ def test_set_invalid_joint_raises(self, default_torque_set: JointTorqueSet) -> N
with pytest.raises(ValueError, match="Unknown joint"):
default_torque_set.set_max_torque("nonexistent", 100.0)
- def test_set_negative_torque_raises(self, default_torque_set: JointTorqueSet) -> None:
+ def test_set_negative_torque_raises(
+ self, default_torque_set: JointTorqueSet
+ ) -> None:
with pytest.raises(ValueError, match="tau_max"):
default_torque_set.set_max_torque("knee", -10.0)
@@ -307,7 +311,9 @@ def test_available_torques_shape(self, default_torque_set: JointTorqueSet) -> No
assert result.shape == (3,)
assert np.all(result > 0)
- def test_available_torques_batch_shape(self, default_torque_set: JointTorqueSet) -> None:
+ def test_available_torques_batch_shape(
+ self, default_torque_set: JointTorqueSet
+ ) -> None:
n = 10
q = np.tile([0.0, -0.5, 0.5], (n, 1))
qd = np.zeros((n, 3))
@@ -341,7 +347,9 @@ def test_find_sticking_point(self, default_torque_set: JointTorqueSet) -> None:
torques = np.ones((n, 3)) * 10.0
torques[3, 1] = 500.0 # knee at step 3
- time_idx, joint_name, peak_util = default_torque_set.find_sticking_point(q, qd, torques)
+ time_idx, joint_name, peak_util = default_torque_set.find_sticking_point(
+ q, qd, torques
+ )
assert time_idx == 3
assert joint_name == "knee"
assert peak_util > 1.0 # should be overloaded
diff --git a/src/movement_optimizer/tests/test_main_window.py b/src/movement_optimizer/tests/test_main_window.py
index b7e5924bca..2402dbcc93 100644
--- a/src/movement_optimizer/tests/test_main_window.py
+++ b/src/movement_optimizer/tests/test_main_window.py
@@ -147,7 +147,11 @@ def stall_label_set_visible(self, v: bool) -> None:
pass
def get_optimization_params(self) -> tuple[float, float, float]:
- return (self.bar_slider.value(), self.dur_slider.value(), self.smooth_slider.value())
+ return (
+ self.bar_slider.value(),
+ self.dur_slider.value(),
+ self.smooth_slider.value(),
+ )
def get_segment_multipliers(self) -> dict[str, float]:
return {
@@ -201,7 +205,9 @@ def draw_all_plots(
) -> None:
self.draw_all_plots_calls.append((result, body, bar, exercise_type))
- def draw_anim_frame(self, fi: int, result: Any, dyn: Any, body: Any, etype: str) -> None:
+ def draw_anim_frame(
+ self, fi: int, result: Any, dyn: Any, body: Any, etype: str
+ ) -> None:
self.draw_anim_frame_calls.append((fi, result, dyn, body, etype))
@@ -219,7 +225,9 @@ def __init__(self) -> None:
from movement_optimizer.gui.exercise_state import ExerciseRuntimeState
from movement_optimizer.trajectory import SolutionCache
- self.exercise_states = [ExerciseRuntimeState() for _name, _etype in self.EXERCISE_CONFIGS]
+ self.exercise_states = [
+ ExerciseRuntimeState() for _name, _etype in self.EXERCISE_CONFIGS
+ ]
self.sidebar = _FakeSidebar()
self.status_label = _FakeLabel()
self.exercise_tabs = [_FakeTab() for _ in self.EXERCISE_CONFIGS]
@@ -493,7 +501,14 @@ def test_squat_returns_correct_etype(self) -> None:
from movement_optimizer.gui.optimization_mixin import OptimizationMixin
window = _FakeWindow()
- _body, _dyn, etype, _bar, _dur, _smoothness = OptimizationMixin._resolve_exercise_params(
+ (
+ _body,
+ _dyn,
+ etype,
+ _bar,
+ _dur,
+ _smoothness,
+ ) = OptimizationMixin._resolve_exercise_params(
window,
0, # type: ignore
) # type: ignore[arg-type]
@@ -503,7 +518,14 @@ def test_deadlift_returns_correct_etype(self) -> None:
from movement_optimizer.gui.optimization_mixin import OptimizationMixin
window = _FakeWindow()
- _body, _dyn, etype, _bar, _dur, _smoothness = OptimizationMixin._resolve_exercise_params(
+ (
+ _body,
+ _dyn,
+ etype,
+ _bar,
+ _dur,
+ _smoothness,
+ ) = OptimizationMixin._resolve_exercise_params(
window,
2, # type: ignore
) # type: ignore[arg-type]
@@ -528,7 +550,14 @@ def test_bar_value_from_slider(self) -> None:
window = _FakeWindow()
window.sidebar.bar_slider.current = 100.0
- _body, _dyn, _etype, bar, _dur, _smoothness = OptimizationMixin._resolve_exercise_params(
+ (
+ _body,
+ _dyn,
+ _etype,
+ bar,
+ _dur,
+ _smoothness,
+ ) = OptimizationMixin._resolve_exercise_params(
window,
0, # type: ignore
) # type: ignore[arg-type]
@@ -540,7 +569,14 @@ def test_full_squat_minimum_duration_enforced(self) -> None:
window = _FakeWindow()
window.sidebar.dur_slider.current = 1.0
- _body, _dyn, _etype, _bar, dur, _smoothness = OptimizationMixin._resolve_exercise_params(
+ (
+ _body,
+ _dyn,
+ _etype,
+ _bar,
+ dur,
+ _smoothness,
+ ) = OptimizationMixin._resolve_exercise_params(
window,
1, # type: ignore
) # type: ignore[arg-type]
diff --git a/src/movement_optimizer/tests/test_models.py b/src/movement_optimizer/tests/test_models.py
index 6e00fd0c3e..1dee618029 100644
--- a/src/movement_optimizer/tests/test_models.py
+++ b/src/movement_optimizer/tests/test_models.py
@@ -59,9 +59,9 @@ def test_negative_bar_height_raises(self) -> None:
def test_mass_fractions_sum_to_one(self) -> None:
"""MASS_FRAC values must sum to exactly 1.0 (issue #125)."""
total = sum(MASS_FRAC.values())
- assert total == pytest.approx(1.0, abs=1e-9), (
- f"MASS_FRAC values sum to {total}, expected 1.0"
- )
+ assert total == pytest.approx(
+ 1.0, abs=1e-9
+ ), f"MASS_FRAC values sum to {total}, expected 1.0"
def test_mass_fractions_sum(self, default_body: BodyModel) -> None:
total = default_body.m_feet + default_body.m_squat.sum()
@@ -84,7 +84,9 @@ def test_inner_bos_is_60_percent(self, default_body: BodyModel) -> None:
b = default_body
full_span = b.toe_x - b.heel_x
inner_span = b.inner_toe - b.inner_heel
- np.testing.assert_allclose(inner_span / full_span, BOS_INNER_FRACTION, atol=1e-10)
+ np.testing.assert_allclose(
+ inner_span / full_span, BOS_INNER_FRACTION, atol=1e-10
+ )
def test_inner_center_between_bounds(self, default_body: BodyModel) -> None:
b = default_body
@@ -213,7 +215,9 @@ def test_deadlift_bar_below_shoulder(self, deadlift_dynamics) -> None:
bp = dyn.bar_position(qs, "deadlift")
assert bp[1] < fk["shoulder"][1]
- def test_deadlift_start_bar_near_ground(self, deadlift_dynamics, default_body) -> None:
+ def test_deadlift_start_bar_near_ground(
+ self, deadlift_dynamics, default_body
+ ) -> None:
dyn, qs, _, _ = deadlift_dynamics
bp = dyn.bar_position(qs, "deadlift")
assert abs(bp[1] - PLATE_RADIUS_STD_M) < 0.15
@@ -237,7 +241,9 @@ def test_batch_torques_match_loop(self, squat_dynamics) -> None:
qd = np.random.default_rng(43).normal(0, 0.5, (n, 3))
qdd = np.random.default_rng(44).normal(0, 1.0, (n, 3))
- loop_torques = np.array([dyn.inverse_dynamics(q[i], qd[i], qdd[i]) for i in range(n)])
+ loop_torques = np.array(
+ [dyn.inverse_dynamics(q[i], qd[i], qdd[i]) for i in range(n)]
+ )
batch_torques = dyn.inverse_dynamics_batch(q, qd, qdd)
np.testing.assert_allclose(batch_torques, loop_torques, rtol=1e-10)
@@ -280,12 +286,12 @@ def test_squat_endpoints_com_in_inner_bos(self, default_body) -> None:
com_start = dyn.com_position(qs, "squat", 60.0)[0]
com_end = dyn.com_position(qe, "squat", 60.0)[0]
b = default_body
- assert b.inner_heel <= com_start <= b.inner_toe, (
- f"Start COM {com_start:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
- )
- assert b.inner_heel <= com_end <= b.inner_toe, (
- f"End COM {com_end:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
- )
+ assert (
+ b.inner_heel <= com_start <= b.inner_toe
+ ), f"Start COM {com_start:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
+ assert (
+ b.inner_heel <= com_end <= b.inner_toe
+ ), f"End COM {com_end:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
def test_full_squat_via_com_in_inner_bos(self, default_body) -> None:
"""Via-point should have COM in the inner 60% zone."""
@@ -294,9 +300,9 @@ def test_full_squat_via_com_in_inner_bos(self, default_body) -> None:
dyn, _, _, _, q_via = make_full_squat_config(default_body, 60.0)
com_via = dyn.com_position(q_via, "full_squat", 60.0)[0]
b = default_body
- assert b.inner_heel <= com_via <= b.inner_toe, (
- f"Via COM {com_via:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
- )
+ assert (
+ b.inner_heel <= com_via <= b.inner_toe
+ ), f"Via COM {com_via:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
def test_deadlift_endpoints_com_in_inner_bos(self, default_body) -> None:
"""Deadlift start and end should have COM in the inner 60% zone."""
@@ -306,12 +312,12 @@ def test_deadlift_endpoints_com_in_inner_bos(self, default_body) -> None:
com_start = dyn.com_position(qs, "deadlift", 60.0)[0]
com_end = dyn.com_position(qe, "deadlift", 60.0)[0]
b = default_body
- assert b.inner_heel <= com_start <= b.inner_toe, (
- f"Start COM {com_start:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
- )
- assert b.inner_heel <= com_end <= b.inner_toe, (
- f"End COM {com_end:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
- )
+ assert (
+ b.inner_heel <= com_start <= b.inner_toe
+ ), f"Start COM {com_start:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
+ assert (
+ b.inner_heel <= com_end <= b.inner_toe
+ ), f"End COM {com_end:.4f} outside inner BOS [{b.inner_heel:.4f}, {b.inner_toe:.4f}]"
class TestLegAbductionCorrection:
@@ -343,8 +349,12 @@ def test_standing_height_decreases(self) -> None:
"""FK shoulder height at standing decreases with abduction."""
body_0 = BodyModel(75.0, 1.75, abduction_angle=0.0)
body_30 = BodyModel(75.0, 1.75, abduction_angle=30.0)
- dyn_0 = LagrangianDynamics(body_0, body_0.m_squat.copy(), body_0.I_squat.copy(), 0.0)
- dyn_30 = LagrangianDynamics(body_30, body_30.m_squat.copy(), body_30.I_squat.copy(), 0.0)
+ dyn_0 = LagrangianDynamics(
+ body_0, body_0.m_squat.copy(), body_0.I_squat.copy(), 0.0
+ )
+ dyn_30 = LagrangianDynamics(
+ body_30, body_30.m_squat.copy(), body_30.I_squat.copy(), 0.0
+ )
fk_0 = dyn_0.forward_kinematics(np.zeros(3))
fk_30 = dyn_30.forward_kinematics(np.zeros(3))
# With abduction, projected leg lengths are shorter, so shoulder is lower
@@ -354,8 +364,12 @@ def test_com_y_decreases(self) -> None:
"""COM y-position decreases with abduction at standing."""
body_0 = BodyModel(75.0, 1.75, abduction_angle=0.0)
body_30 = BodyModel(75.0, 1.75, abduction_angle=30.0)
- dyn_0 = LagrangianDynamics(body_0, body_0.m_squat.copy(), body_0.I_squat.copy(), 0.0)
- dyn_30 = LagrangianDynamics(body_30, body_30.m_squat.copy(), body_30.I_squat.copy(), 0.0)
+ dyn_0 = LagrangianDynamics(
+ body_0, body_0.m_squat.copy(), body_0.I_squat.copy(), 0.0
+ )
+ dyn_30 = LagrangianDynamics(
+ body_30, body_30.m_squat.copy(), body_30.I_squat.copy(), 0.0
+ )
com_0 = dyn_0.com_position(np.zeros(3), "squat", 0.0)
com_30 = dyn_30.com_position(np.zeros(3), "squat", 0.0)
assert com_30[1] < com_0[1]
diff --git a/src/movement_optimizer/tests/test_motion_analysis_panel.py b/src/movement_optimizer/tests/test_motion_analysis_panel.py
index 58f5a42ada..707273e734 100644
--- a/src/movement_optimizer/tests/test_motion_analysis_panel.py
+++ b/src/movement_optimizer/tests/test_motion_analysis_panel.py
@@ -164,8 +164,12 @@ def test_panel_mode_suppresses_data_axis_legends(self, chain_history) -> None:
plotters = (
lambda ax: plot_chain_tension(ax, chain_history, legend=False),
lambda ax: plot_chain_curvature(ax, chain_history, legend=False),
- lambda ax: plot_chain_energy(ax, np.linspace(0, 1, _T), np.zeros(_T), legend=False),
- lambda ax: plot_chain_tip_speed(ax, np.linspace(0, 1, _T), np.zeros(_T), legend=False),
+ lambda ax: plot_chain_energy(
+ ax, np.linspace(0, 1, _T), np.zeros(_T), legend=False
+ ),
+ lambda ax: plot_chain_tip_speed(
+ ax, np.linspace(0, 1, _T), np.zeros(_T), legend=False
+ ),
)
for plotter in plotters:
figure = Figure()
diff --git a/src/movement_optimizer/tests/test_motion_analysis_panel_legends.py b/src/movement_optimizer/tests/test_motion_analysis_panel_legends.py
index 8ee9f832c1..b037301b99 100644
--- a/src/movement_optimizer/tests/test_motion_analysis_panel_legends.py
+++ b/src/movement_optimizer/tests/test_motion_analysis_panel_legends.py
@@ -126,13 +126,17 @@ def test_swingset_minimum_layout_preserves_curve_height(qapp, swing_history) ->
panel.draw()
panel.canvas.draw()
renderer = panel.canvas.get_renderer()
- data_heights = [axes.get_window_extent(renderer).height for axes in panel.axes.values()]
+ data_heights = [
+ axes.get_window_extent(renderer).height for axes in panel.axes.values()
+ ]
assert min(data_heights) >= 210.0
_assert_panel_legends_do_not_cover_plots(panel)
-def test_swingset_live_tab_layout_preserves_usable_plot_width(qapp, swing_history) -> None:
+def test_swingset_live_tab_layout_preserves_usable_plot_width(
+ qapp, swing_history
+) -> None:
from movement_optimizer.gui.motion_analysis_panel import MotionAnalysisPanel
panel = MotionAnalysisPanel(
@@ -149,7 +153,9 @@ def test_swingset_live_tab_layout_preserves_usable_plot_width(qapp, swing_histor
)
panel.draw()
renderer = panel.canvas.get_renderer()
- data_widths = [axes.get_window_extent(renderer).width for axes in panel.axes.values()]
+ data_widths = [
+ axes.get_window_extent(renderer).width for axes in panel.axes.values()
+ ]
assert min(data_widths) >= 300.0
_assert_panel_legends_do_not_cover_plots(
@@ -181,7 +187,8 @@ def test_swingset_legends_are_docked_in_reserved_rows(qapp, swing_history) -> No
assert legend_box.x0 >= figure_box.x0 - 1.0
assert legend_box.x1 <= figure_box.x1 + 1.0
assert not any(
- legend_box.overlaps(axes.get_window_extent(renderer)) for axes in panel.axes.values()
+ legend_box.overlaps(axes.get_window_extent(renderer))
+ for axes in panel.axes.values()
)
@@ -217,7 +224,9 @@ def test_swingset_docked_legends_clear_minimum_plot_size(qapp, swing_history) ->
assert all(axes.get_legend() is None for axes in panel.axes.values())
-def test_swingset_docked_legends_clear_compressed_plot_size(qapp, swing_history) -> None:
+def test_swingset_docked_legends_clear_compressed_plot_size(
+ qapp, swing_history
+) -> None:
from movement_optimizer.gui.motion_analysis_panel import MotionAnalysisPanel
panel = MotionAnalysisPanel(
@@ -231,7 +240,9 @@ def test_swingset_docked_legends_clear_compressed_plot_size(qapp, swing_history)
assert all(axes.get_legend() is None for axes in panel.axes.values())
-def test_draw_enforces_minimum_render_size_before_docking_legends(qapp, swing_history) -> None:
+def test_draw_enforces_minimum_render_size_before_docking_legends(
+ qapp, swing_history
+) -> None:
from movement_optimizer.gui.motion_analysis_panel import MotionAnalysisPanel
panel = MotionAnalysisPanel(
@@ -252,7 +263,9 @@ def test_draw_enforces_minimum_render_size_before_docking_legends(qapp, swing_hi
def test_chain_legends_are_docked_outside_data_axes(qapp, chain_history) -> None:
from movement_optimizer.gui.motion_analysis_panel import MotionAnalysisPanel
- panel = MotionAnalysisPanel(["tension", "curvature", "energy", "tip_speed"], rows=2, cols=2)
+ panel = MotionAnalysisPanel(
+ ["tension", "curvature", "energy", "tip_speed"], rows=2, cols=2
+ )
plot_chain_tension(panel.axes["tension"], chain_history)
plot_chain_curvature(panel.axes["curvature"], chain_history)
plot_chain_energy(panel.axes["energy"], np.linspace(0, 1, _T), np.zeros(_T))
diff --git a/src/movement_optimizer/tests/test_motion_tabs.py b/src/movement_optimizer/tests/test_motion_tabs.py
index 736efbe135..1940aa7abd 100644
--- a/src/movement_optimizer/tests/test_motion_tabs.py
+++ b/src/movement_optimizer/tests/test_motion_tabs.py
@@ -25,7 +25,10 @@
)
from movement_optimizer.gui import motion_tabs, motion_tabs_chain, policy_worker
-from movement_optimizer.gui.app_icon import movement_optimizer_icon, movement_optimizer_icon_path
+from movement_optimizer.gui.app_icon import (
+ movement_optimizer_icon,
+ movement_optimizer_icon_path,
+)
from movement_optimizer.gui.main_window import MainWindow
from movement_optimizer.gui.motion_tabs import (
ChainDynamicsTab,
@@ -36,7 +39,9 @@
from movement_optimizer.gui.policy_trace_canvas import PolicyTraceCanvas
-def _wait_for_policy_worker(qapp, swingset: SwingsetTab, timeout_s: float = 10.0) -> None:
+def _wait_for_policy_worker(
+ qapp, swingset: SwingsetTab, timeout_s: float = 10.0
+) -> None:
deadline = time.monotonic() + timeout_s
while swingset._policy_worker is not None and time.monotonic() < deadline:
qapp.processEvents()
@@ -70,7 +75,9 @@ def _assert_reserved_legend_rows_do_not_cover_plots(panel) -> None:
def test_main_window_preserves_barbell_tabs_and_adds_motion_tabs(qapp) -> None:
window = MainWindow()
- tab_names = [window.tabs.tabText(index).strip() for index in range(window.tabs.count())]
+ tab_names = [
+ window.tabs.tabText(index).strip() for index in range(window.tabs.count())
+ ]
assert tab_names[:7] == [
"Bottoms Up Squat",
@@ -216,7 +223,9 @@ def test_swingset_tab_exposes_policy_tuning_and_progress(qapp) -> None:
"phase_samples",
):
assert key in swingset._controls
- swingset.iterative_checkbox.setChecked(False) # exercise the grid-search fallback path.
+ swingset.iterative_checkbox.setChecked(
+ False
+ ) # exercise the grid-search fallback path.
swingset._controls["cycles"].set_value(1)
swingset._controls["freq_samples"].set_value(2)
swingset._controls["hip_samples"].set_value(1)
@@ -240,7 +249,9 @@ def test_swingset_policy_terminology_is_not_walking(qapp) -> None:
swingset = SwingsetTab()
visible_text = " ".join(
- widget.text() for widget in swingset.findChildren((QLabel, QPushButton)) if widget.text()
+ widget.text()
+ for widget in swingset.findChildren((QLabel, QPushButton))
+ if widget.text()
)
assert "walking" not in visible_text.lower()
@@ -255,7 +266,9 @@ def test_motion_tab_parameter_panels_are_scrollable_and_not_compressed(qapp) ->
assert scroll_area is not None
assert scroll_area.widgetResizable()
assert tab.control_panel_visible()
- assert all(line_edit.minimumHeight() >= 28 for line_edit in tab.findChildren(QLineEdit))
+ assert all(
+ line_edit.minimumHeight() >= 28 for line_edit in tab.findChildren(QLineEdit)
+ )
tab.set_control_panel_visible(False)
assert not tab.control_panel_visible()
@@ -272,7 +285,9 @@ def test_swingset_optimize_policy_action_is_sticky_above_scroll_area(qapp) -> No
assert swingset.optimize_button.property("class") == "primary"
assert swingset.optimize_button.minimumHeight() >= 48
assert swingset.optimize_button.minimumWidth() >= 220
- assert swingset.optimize_button not in scroll_area.widget().findChildren(QPushButton)
+ assert swingset.optimize_button not in scroll_area.widget().findChildren(
+ QPushButton
+ )
def test_swingset_autoplay_after_policy_optimization_is_configurable(qapp) -> None:
@@ -298,7 +313,9 @@ def test_swingset_autoplay_after_policy_optimization_is_configurable(qapp) -> No
def test_swingset_policy_trace_canvas_accepts_optimization_samples(qapp) -> None:
swingset = SwingsetTab()
swingset.autoplay_checkbox.setChecked(False)
- swingset.iterative_checkbox.setChecked(False) # exercise the grid-search fallback path.
+ swingset.iterative_checkbox.setChecked(
+ False
+ ) # exercise the grid-search fallback path.
swingset._controls["cycles"].set_value(1)
swingset._controls["freq_samples"].set_value(2)
swingset._controls["hip_samples"].set_value(1)
@@ -325,7 +342,9 @@ def test_swingset_policy_trace_canvas_handles_sparse_series(qapp) -> None:
pixmap = QPixmap(120, 80)
painter = QPainter(pixmap)
try:
- swingset.policy_trace_canvas._draw_normalized_series(painter, "missing", QColor("white"), 1)
+ swingset.policy_trace_canvas._draw_normalized_series(
+ painter, "missing", QColor("white"), 1
+ )
finally:
painter.end()
@@ -584,7 +603,9 @@ def test_chain_rollout_keeps_physical_anchor_fixed(qapp) -> None:
np.testing.assert_allclose(chain._rollout.positions[:, 0, :], 0.0)
-def test_chain_tab_reports_invalid_inputs_and_covers_playback_branches(qapp, monkeypatch) -> None:
+def test_chain_tab_reports_invalid_inputs_and_covers_playback_branches(
+ qapp, monkeypatch
+) -> None:
chain = ChainDynamicsTab()
chain.autoplay_checkbox.setChecked(False)
chain.tie_segments.setChecked(False)
@@ -649,10 +670,13 @@ def test_swingset_iterative_optimize_populates_panel_and_overlays(qapp) -> None:
assert 0 < swingset.policy_trace_canvas.sample_count() <= 50
# Analysis plots populated.
assert swingset.analysis_panel.axes["torques"].get_lines()
- assert all(axes.get_legend() is None for axes in swingset.analysis_panel.axes.values())
+ assert all(
+ axes.get_legend() is None for axes in swingset.analysis_panel.axes.values()
+ )
assert swingset.analysis_panel._figure_legend is None
assert any(
- axes.get_legend() is not None for axes in swingset.analysis_panel.legend_axes.values()
+ axes.get_legend() is not None
+ for axes in swingset.analysis_panel.legend_axes.values()
)
# Force overlay drawn (all toggles default-on).
assert swingset.canvas._overlay.arrows or swingset.canvas._overlay.com_markers
@@ -712,7 +736,9 @@ def test_swingset_playback_uses_cached_force_fields(qapp, monkeypatch) -> None:
_wait_for_policy_worker(qapp, swingset)
def fail_recompute(*_args, **_kwargs):
- raise AssertionError("playback must not recompute rollout-wide swing force fields")
+ raise AssertionError(
+ "playback must not recompute rollout-wide swing force fields"
+ )
monkeypatch.setattr(motion_tabs, "swing_force_fields", fail_recompute)
@@ -739,7 +765,10 @@ def test_chain_simulate_populates_panel_and_overlays(qapp) -> None:
assert chain.analysis_panel.axes["tension"].get_lines()
assert all(axes.get_legend() is None for axes in chain.analysis_panel.axes.values())
assert chain.analysis_panel._figure_legend is None
- assert any(axes.get_legend() is not None for axes in chain.analysis_panel.legend_axes.values())
+ assert any(
+ axes.get_legend() is not None
+ for axes in chain.analysis_panel.legend_axes.values()
+ )
assert chain.canvas._overlay.arrows
@@ -781,7 +810,9 @@ def test_chain_playback_uses_cached_force_fields(qapp, monkeypatch) -> None:
chain._simulate()
def fail_recompute(*_args, **_kwargs):
- raise AssertionError("playback must not recompute rollout-wide chain force fields")
+ raise AssertionError(
+ "playback must not recompute rollout-wide chain force fields"
+ )
monkeypatch.setattr(motion_tabs_chain, "chain_force_fields", fail_recompute)
@@ -974,6 +1005,8 @@ def test_policy_trace_iteration_label_stays_below_plot_area(qapp) -> None:
label_rect = trace._iteration_label_rect()
- assert label_rect.top() >= trace._plot_bottom() + trace._AXIS_LABEL_TOP_PADDING_PX - 1
+ assert (
+ label_rect.top() >= trace._plot_bottom() + trace._AXIS_LABEL_TOP_PADDING_PX - 1
+ )
assert trace._plot_bottom() - trace._top_margin() >= trace._MINIMUM_PLOT_HEIGHT_PX
trace.grab() # repaint with bottom-axis label must not raise
diff --git a/src/movement_optimizer/tests/test_optimization_mixin.py b/src/movement_optimizer/tests/test_optimization_mixin.py
index dfca8ca92e..93cc8c7d1c 100644
--- a/src/movement_optimizer/tests/test_optimization_mixin.py
+++ b/src/movement_optimizer/tests/test_optimization_mixin.py
@@ -65,7 +65,9 @@ def test_on_cancelled_resets_state(window) -> None:
def test_on_err_with_structured_and_plain_errors(window) -> None:
window._opt_running = True
- window._on_err(OptimizationError("boom", error_code="OPT_X", suggestion="try again"))
+ window._on_err(
+ OptimizationError("boom", error_code="OPT_X", suggestion="try again")
+ )
assert "OPT_X" in window.status_label.text()
window._on_err("plain failure")
assert "plain failure" in window.status_label.text()
@@ -116,7 +118,9 @@ def test_completed_single_exercise_autoplays_when_enabled(window, monkeypatch) -
def test_finish_or_chain_advances_then_chain(window, monkeypatch) -> None:
calls: list[tuple[int, list[int] | None]] = []
- monkeypatch.setattr(window, "_run_exercise", lambda idx, rest=None: calls.append((idx, rest)))
+ monkeypatch.setattr(
+ window, "_run_exercise", lambda idx, rest=None: calls.append((idx, rest))
+ )
window._finish_or_chain([1, 2], "msg")
assert calls == [(1, [2])]
diff --git a/src/movement_optimizer/tests/test_parameter_sidebar.py b/src/movement_optimizer/tests/test_parameter_sidebar.py
index ef7feeb259..8f49e55bd1 100644
--- a/src/movement_optimizer/tests/test_parameter_sidebar.py
+++ b/src/movement_optimizer/tests/test_parameter_sidebar.py
@@ -32,7 +32,9 @@ def test_action_handlers_connect_and_emit(sidebar) -> None:
"compare_trials_requested",
"clear_comparison_requested",
]
- sidebar.connect_action_handlers({name: (lambda n=name: fired.append(n)) for name in names})
+ sidebar.connect_action_handlers(
+ {name: (lambda n=name: fired.append(n)) for name in names}
+ )
for name in names:
getattr(sidebar, name).emit()
assert set(fired) == set(names)
diff --git a/src/movement_optimizer/tests/test_plot_renderer.py b/src/movement_optimizer/tests/test_plot_renderer.py
index 4de8313f64..49b2bcbadd 100644
--- a/src/movement_optimizer/tests/test_plot_renderer.py
+++ b/src/movement_optimizer/tests/test_plot_renderer.py
@@ -72,7 +72,9 @@ def test_plot_angles(self, mock_ax, dummy_result):
plot_angles(mock_ax, dummy_result)
assert mock_ax.plot.call_count == 3
mock_ax.set_title.assert_called_once_with(
- "Joint Angles", color=mock_ax.set_title.call_args[1].get("color"), fontsize=10
+ "Joint Angles",
+ color=mock_ax.set_title.call_args[1].get("color"),
+ fontsize=10,
)
def test_plot_torques(self, mock_ax, dummy_result):
@@ -117,7 +119,9 @@ def test_plot_com_balance(self, mock_ax, dummy_result, body):
def test_plot_spine_loads(self, mock_ax, dummy_result, body):
ax_comp = MagicMock()
ax_shear = MagicMock()
- plot_spine_loads(ax_comp, ax_shear, dummy_result, body, bar_mass=20.0, name="squat")
+ plot_spine_loads(
+ ax_comp, ax_shear, dummy_result, body, bar_mass=20.0, name="squat"
+ )
ax_comp.plot.assert_called_once()
ax_comp.axhline.assert_called_once()
@@ -200,6 +204,8 @@ def test_bottoms_up_squat_is_aliased_to_squat(self, dummy_result, body):
ax_comp = MagicMock()
ax_shear = MagicMock()
- plot_spine_loads(ax_comp, ax_shear, dummy_result, body, 60.0, "Bottoms Up Squat")
+ plot_spine_loads(
+ ax_comp, ax_shear, dummy_result, body, 60.0, "Bottoms Up Squat"
+ )
assert ax_comp.plot.called
assert ax_shear.plot.called
diff --git a/src/movement_optimizer/tests/test_rust_parity_com_x.py b/src/movement_optimizer/tests/test_rust_parity_com_x.py
index 962fff7670..a6ea9a0629 100644
--- a/src/movement_optimizer/tests/test_rust_parity_com_x.py
+++ b/src/movement_optimizer/tests/test_rust_parity_com_x.py
@@ -42,7 +42,9 @@ def _make_deadlift_dynamics() -> LagrangianDynamics:
"""Deadlift dynamics (arm mass folded into the load, no bar offset)."""
body = BodyModel(75.0, 1.75)
load = body.m_arms + 100.0
- return LagrangianDynamics(body, body.m_deadlift.copy(), body.I_deadlift.copy(), load)
+ return LagrangianDynamics(
+ body, body.m_deadlift.copy(), body.I_deadlift.copy(), load
+ )
def _random_q(rng: np.random.Generator, n: int) -> np.ndarray:
diff --git a/src/movement_optimizer/tests/test_scipy_dependency_contract.py b/src/movement_optimizer/tests/test_scipy_dependency_contract.py
index 8f43d5880d..3e1a4ff50b 100644
--- a/src/movement_optimizer/tests/test_scipy_dependency_contract.py
+++ b/src/movement_optimizer/tests/test_scipy_dependency_contract.py
@@ -14,8 +14,12 @@
def test_scipy_dependency_has_no_legacy_1_16_ceiling() -> None:
- pyproject = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
- scipy_specs = [dep for dep in pyproject["project"]["dependencies"] if dep.startswith("scipy")]
+ pyproject = tomllib.loads(
+ (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
+ )
+ scipy_specs = [
+ dep for dep in pyproject["project"]["dependencies"] if dep.startswith("scipy")
+ ]
assert scipy_specs == ["scipy>=1.10"]
diff --git a/src/movement_optimizer/tests/test_shared_theme_dependency.py b/src/movement_optimizer/tests/test_shared_theme_dependency.py
index 0472753200..b594050f12 100644
--- a/src/movement_optimizer/tests/test_shared_theme_dependency.py
+++ b/src/movement_optimizer/tests/test_shared_theme_dependency.py
@@ -34,7 +34,15 @@ def test_shared_theme_public_surface_is_importable() -> None:
# The themes we map onto must exist with the keys the Palette consumes.
assert "Dark" in BUILTIN_THEMES
assert "Light" in BUILTIN_THEMES
- required = {"bg", "group_bg", "input_bg", "text", "text_secondary", "accent", "button_hover"}
+ required = {
+ "bg",
+ "group_bg",
+ "input_bg",
+ "text",
+ "text_secondary",
+ "accent",
+ "button_hover",
+ }
assert required.issubset(set(THEME_COLOR_KEYS))
assert required.issubset(set(BUILTIN_THEMES["Dark"]))
diff --git a/src/movement_optimizer/tests/test_spine_loads.py b/src/movement_optimizer/tests/test_spine_loads.py
index cdc0a367b4..81726deb01 100644
--- a/src/movement_optimizer/tests/test_spine_loads.py
+++ b/src/movement_optimizer/tests/test_spine_loads.py
@@ -28,7 +28,9 @@ def squat_dyn(default_body: BodyModel):
class TestStandingCompression:
"""At standing (q=0, qd=0, qdd=0) compression should equal gravity on mass above L5."""
- def test_standing_compression_equals_gravity(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_standing_compression_equals_gravity(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
q = np.zeros(3)
qd = np.zeros(3)
qdd = np.zeros(3)
@@ -41,7 +43,9 @@ def test_standing_compression_equals_gravity(self, default_body: BodyModel, squa
expected = (m_above + bar_mass) * default_body.g
np.testing.assert_allclose(comp, expected, rtol=1e-6)
- def test_standing_compression_no_bar(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_standing_compression_no_bar(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
q = np.zeros(3)
qd = np.zeros(3)
qdd = np.zeros(3)
@@ -68,7 +72,9 @@ def test_standing_shear_near_zero(self, default_body: BodyModel, squat_dyn) -> N
class TestForwardLean:
"""With torso lean, shear increases and compression decreases."""
- def test_shear_increases_with_lean(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_shear_increases_with_lean(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
qd = np.zeros(3)
qdd = np.zeros(3)
bar_mass = 60.0
@@ -76,12 +82,16 @@ def test_shear_increases_with_lean(self, default_body: BodyModel, squat_dyn) ->
q_upright = np.array([0.0, 0.0, 0.0])
q_leaned = np.array([0.0, 0.0, np.radians(30)])
- shear_upright = spinal_shear(q_upright, qd, qdd, default_body, bar_mass, "squat")
+ shear_upright = spinal_shear(
+ q_upright, qd, qdd, default_body, bar_mass, "squat"
+ )
shear_leaned = spinal_shear(q_leaned, qd, qdd, default_body, bar_mass, "squat")
assert abs(shear_leaned) > abs(shear_upright) # type: ignore
- def test_shear_proportional_to_sin(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_shear_proportional_to_sin(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
qd = np.zeros(3)
qdd = np.zeros(3)
bar_mass = 60.0
@@ -94,7 +104,9 @@ def test_shear_proportional_to_sin(self, default_body: BodyModel, squat_dyn) ->
expected = (m_above + bar_mass) * default_body.g * np.sin(angle)
np.testing.assert_allclose(shear, expected, rtol=1e-6)
- def test_compression_decreases_with_lean(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_compression_decreases_with_lean(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
qd = np.zeros(3)
qdd = np.zeros(3)
bar_mass = 60.0
@@ -102,12 +114,18 @@ def test_compression_decreases_with_lean(self, default_body: BodyModel, squat_dy
q_upright = np.array([0.0, 0.0, 0.0])
q_leaned = np.array([0.0, 0.0, np.radians(30)])
- comp_upright = spinal_compression(q_upright, qd, qdd, default_body, bar_mass, "squat")
- comp_leaned = spinal_compression(q_leaned, qd, qdd, default_body, bar_mass, "squat")
+ comp_upright = spinal_compression(
+ q_upright, qd, qdd, default_body, bar_mass, "squat"
+ )
+ comp_leaned = spinal_compression(
+ q_leaned, qd, qdd, default_body, bar_mass, "squat"
+ )
assert comp_leaned < comp_upright
- def test_compression_cos_component(self, default_body: BodyModel, squat_dyn) -> None:
+ def test_compression_cos_component(
+ self, default_body: BodyModel, squat_dyn
+ ) -> None:
qd = np.zeros(3)
qdd = np.zeros(3)
bar_mass = 60.0
@@ -155,7 +173,10 @@ def test_batch_matches_loop(self, default_body: BodyModel, squat_dyn) -> None:
batch_comp = spinal_compression(q, qd, qdd, default_body, 60.0, "squat")
loop_comp = np.array(
- [spinal_compression(q[i], qd[i], qdd[i], default_body, 60.0, "squat") for i in range(n)]
+ [
+ spinal_compression(q[i], qd[i], qdd[i], default_body, 60.0, "squat")
+ for i in range(n)
+ ]
)
np.testing.assert_allclose(batch_comp, loop_comp, rtol=1e-10)
@@ -220,7 +241,9 @@ def test_shear_exceeds_static_during_motion(self, default_body: BodyModel) -> No
q = np.array([0.0, 0.0, angle])
bar_mass = 60.0
- static_shear = spinal_shear(q, np.zeros(3), np.zeros(3), default_body, bar_mass, "squat")
+ static_shear = spinal_shear(
+ q, np.zeros(3), np.zeros(3), default_body, bar_mass, "squat"
+ )
dynamic_shear = spinal_shear(
q,
np.array([0.0, 0.0, 3.0]),
diff --git a/src/movement_optimizer/tests/test_subprocess_usage.py b/src/movement_optimizer/tests/test_subprocess_usage.py
index fcbeec55b8..71fedee661 100644
--- a/src/movement_optimizer/tests/test_subprocess_usage.py
+++ b/src/movement_optimizer/tests/test_subprocess_usage.py
@@ -6,7 +6,11 @@
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
-PYTHON_SOURCES = (PROJECT_ROOT / "scripts", PROJECT_ROOT / "src", PROJECT_ROOT / "tests")
+PYTHON_SOURCES = (
+ PROJECT_ROOT / "scripts",
+ PROJECT_ROOT / "src",
+ PROJECT_ROOT / "tests",
+)
def _subprocess_calls(tree: ast.AST) -> list[ast.Call]:
@@ -36,7 +40,9 @@ def test_subprocess_calls_do_not_use_shell_true() -> None:
and isinstance(keyword.value, ast.Constant)
and keyword.value.value is True
):
- offenders.append(f"{source.relative_to(PROJECT_ROOT)}:{call.lineno}")
+ offenders.append(
+ f"{source.relative_to(PROJECT_ROOT)}:{call.lineno}"
+ )
assert offenders == []
@@ -50,7 +56,11 @@ def test_subprocess_calls_use_sequence_arguments() -> None:
if not call.args:
continue
first_arg = call.args[0]
- if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
- offenders.append(f"{source.relative_to(PROJECT_ROOT)}:{call.lineno}")
+ if isinstance(first_arg, ast.Constant) and isinstance(
+ first_arg.value, str
+ ):
+ offenders.append(
+ f"{source.relative_to(PROJECT_ROOT)}:{call.lineno}"
+ )
assert offenders == []
diff --git a/src/movement_optimizer/tests/test_swingset_chain_models.py b/src/movement_optimizer/tests/test_swingset_chain_models.py
index 87f0c9c747..b893ab6c67 100644
--- a/src/movement_optimizer/tests/test_swingset_chain_models.py
+++ b/src/movement_optimizer/tests/test_swingset_chain_models.py
@@ -108,7 +108,9 @@ def test_chain_simulation_damps_energy() -> None:
assert len(rollout.states) == 25
assert rollout.positions.shape == (25, 7, 2)
assert np.all(np.isfinite(rollout.energy_j))
- assert total_energy(config, rollout.states[-1]) == pytest.approx(rollout.energy_j[-1])
+ assert total_energy(config, rollout.states[-1]) == pytest.approx(
+ rollout.energy_j[-1]
+ )
link_lengths = np.linalg.norm(np.diff(rollout.positions, axis=1), axis=2)
np.testing.assert_allclose(link_lengths, config.segment_length_m)
@@ -146,13 +148,15 @@ def test_chain_single_segment_gravity_matches_slender_rod_pendulum() -> None:
)
angle = 0.2
dt_s = 1e-4
- state = ChainState(np.asarray([angle], dtype=np.float64), np.zeros(1, dtype=np.float64))
+ state = ChainState(
+ np.asarray([angle], dtype=np.float64), np.zeros(1, dtype=np.float64)
+ )
stepped = step_chain(config, state, dt_s=dt_s)
- expected_acceleration = -(3.0 * config.gravity_m_s2 / (2.0 * config.segment_length_m)) * np.sin(
- angle
- )
+ expected_acceleration = -(
+ 3.0 * config.gravity_m_s2 / (2.0 * config.segment_length_m)
+ ) * np.sin(angle)
observed_acceleration = stepped.angular_velocities_rad_s[0] / dt_s
assert observed_acceleration == pytest.approx(expected_acceleration, rel=0.02)
@@ -181,8 +185,12 @@ def test_chain_downstream_load_slows_top_link_gravity() -> None:
stepped = step_chain(config, state, dt_s=dt_s)
acceleration = stepped.angular_velocities_rad_s / dt_s
- single_link = -(3.0 * config.gravity_m_s2 / (2.0 * config.segment_length_m)) * np.sin(angle)
- assert acceleration[0] == pytest.approx(single_link / config.segment_count, rel=0.03)
+ single_link = -(
+ 3.0 * config.gravity_m_s2 / (2.0 * config.segment_length_m)
+ ) * np.sin(angle)
+ assert acceleration[0] == pytest.approx(
+ single_link / config.segment_count, rel=0.03
+ )
assert acceleration[-1] == pytest.approx(single_link, rel=0.03)
@@ -197,13 +205,17 @@ def test_chain_tip_kick_velocities_increase_toward_tip() -> None:
def test_chain_random_wadded_start_is_deterministic_and_validated() -> None:
config = ChainConfig(segment_count=5)
- first = random_wadded_chain_state(config, angle_span_rad=np.pi, velocity_span_rad_s=0.4, seed=7)
+ first = random_wadded_chain_state(
+ config, angle_span_rad=np.pi, velocity_span_rad_s=0.4, seed=7
+ )
second = random_wadded_chain_state(
config, angle_span_rad=np.pi, velocity_span_rad_s=0.4, seed=7
)
np.testing.assert_allclose(first.angles_rad, second.angles_rad)
- np.testing.assert_allclose(first.angular_velocities_rad_s, second.angular_velocities_rad_s)
+ np.testing.assert_allclose(
+ first.angular_velocities_rad_s, second.angular_velocities_rad_s
+ )
assert first.angles_rad.shape == (5,)
assert np.max(np.abs(first.angles_rad)) <= np.pi
with pytest.raises(ValueError, match="angle_span_rad"):
@@ -232,7 +244,9 @@ def test_chain_simulation_validates_rollout_inputs() -> None:
with pytest.raises(ValueError, match="dt_s"):
step_chain(config, initial, dt_s=0.0)
with pytest.raises(ValueError, match="incompatible"):
- simulate_chain(config, initial, steps=2, dt_s=0.01, torque_history_nm=np.zeros((2, 2)))
+ simulate_chain(
+ config, initial, steps=2, dt_s=0.01, torque_history_nm=np.zeros((2, 2))
+ )
def test_swingset_snapshot_models_body_chain_and_mass() -> None:
@@ -276,7 +290,9 @@ def test_swingset_elbow_branch_does_not_mirror_when_control_crosses_zero() -> No
elbow = snapshot.points["elbow"]
hand_delta = hand - shoulder
elbow_delta = elbow - shoulder
- branch_signs.append(float(hand_delta[0] * elbow_delta[1] - hand_delta[1] * elbow_delta[0]))
+ branch_signs.append(
+ float(hand_delta[0] * elbow_delta[1] - hand_delta[1] * elbow_delta[0])
+ )
elbow_points.append(elbow)
# The elbow must never mirror to the far branch as the requested flexion
@@ -284,7 +300,9 @@ def test_swingset_elbow_branch_does_not_mirror_when_control_crosses_zero() -> No
assert min(branch_signs) > 0.0
# No discontinuous jump (a mirror flip would be a large step); the elbow
# moves smoothly across the swept range.
- max_step = max(float(np.linalg.norm(end - start)) for start, end in pairwise(elbow_points))
+ max_step = max(
+ float(np.linalg.norm(end - start)) for start, end in pairwise(elbow_points)
+ )
assert max_step < 0.1
@@ -458,7 +476,9 @@ def test_cyclic_policy_controls_match_callback_policy() -> None:
def test_swingset_cyclic_policy_search_selects_height_objective() -> None:
result = optimize_cyclic_policy(SwingSetConfig(), steps=40, dt_s=0.02)
- assert result.objective_height_m == pytest.approx(result.rollout.metrics.max_height_gain_m)
+ assert result.objective_height_m == pytest.approx(
+ result.rollout.metrics.max_height_gain_m
+ )
assert result.objective_height_m > 0.0
assert result.parameters.frequency_hz > 0.0
@@ -486,7 +506,9 @@ def test_swingset_policy_search_reports_progress_and_uses_cycles() -> None:
cycles=2.0,
dt_s=0.02,
search_space=search_space,
- progress_callback=lambda done, total, score, _params: progress.append((done, total, score)),
+ progress_callback=lambda done, total, score, _params: progress.append(
+ (done, total, score)
+ ),
)
assert result.evaluated_candidates == 4
@@ -537,7 +559,9 @@ def test_swingset_joint_torque_estimator_validates_control_history() -> None:
def test_swingset_rollout_validates_inputs() -> None:
config = SwingSetConfig()
with pytest.raises(ValueError, match="steps"):
- simulate_swingset(config, SwingSetState.rest(), 0, 0.02, heuristic_pumping_policy)
+ simulate_swingset(
+ config, SwingSetState.rest(), 0, 0.02, heuristic_pumping_policy
+ )
with pytest.raises(ValueError, match="dt_s"):
step_swingset(config, SwingSetState.rest(), SwingControlAction(), dt_s=0.0)
with pytest.raises(ValueError, match="steps"):
@@ -568,7 +592,9 @@ def test_iterative_optimizer_is_deterministic() -> None:
first = optimize_cyclic_policy_iterative(config, steps=40, budget=60, seed=7)
second = optimize_cyclic_policy_iterative(config, steps=40, budget=60, seed=7)
assert first.objective_height_m == pytest.approx(second.objective_height_m)
- assert first.parameters.frequency_hz == pytest.approx(second.parameters.frequency_hz)
+ assert first.parameters.frequency_hz == pytest.approx(
+ second.parameters.frequency_hz
+ )
assert first.parameters.phase_rad == pytest.approx(second.parameters.phase_rad)
assert len(first.trace) == len(second.trace)
@@ -583,7 +609,9 @@ def test_iterative_optimizer_honors_budget(budget: int) -> None:
def test_iterative_optimizer_matches_or_beats_grid() -> None:
config = SwingSetConfig()
- grid = optimize_cyclic_policy(config, steps=80, search_space=CyclicPolicySearchSpace())
+ grid = optimize_cyclic_policy(
+ config, steps=80, search_space=CyclicPolicySearchSpace()
+ )
iterative = optimize_cyclic_policy_iterative(config, steps=80, budget=400, seed=0)
assert iterative.objective_height_m >= grid.objective_height_m - 0.05
@@ -602,7 +630,9 @@ def test_iterative_optimizer_progress_callback_contract() -> None:
config = SwingSetConfig()
calls: list[tuple[int, int, float]] = []
- def _record(completed: int, total: int, best: float, params: CyclicPolicyParameters) -> None:
+ def _record(
+ completed: int, total: int, best: float, params: CyclicPolicyParameters
+ ) -> None:
calls.append((completed, total, best))
assert isinstance(params, CyclicPolicyParameters)
diff --git a/src/movement_optimizer/tests/test_swingset_forces.py b/src/movement_optimizer/tests/test_swingset_forces.py
index 5772a3403d..e68a4be29c 100644
--- a/src/movement_optimizer/tests/test_swingset_forces.py
+++ b/src/movement_optimizer/tests/test_swingset_forces.py
@@ -74,7 +74,9 @@ def test_swing_chain_tension_uses_acceleration_not_velocity() -> None:
)
linear_com_rollout = dataclasses.replace(rollout, snapshots=snapshots)
- field = swing_force_field(config, linear_com_rollout, DEFAULT_POLICY_DT_S, frame_index=10)
+ field = swing_force_field(
+ config, linear_com_rollout, DEFAULT_POLICY_DT_S, frame_index=10
+ )
np.testing.assert_allclose(field.chain_tension_n, -field.gravity_n, atol=1e-9)
diff --git a/src/movement_optimizer/tests/test_thread_safety.py b/src/movement_optimizer/tests/test_thread_safety.py
index 032dc7e34a..a42c6ebd77 100644
--- a/src/movement_optimizer/tests/test_thread_safety.py
+++ b/src/movement_optimizer/tests/test_thread_safety.py
@@ -225,9 +225,9 @@ def runner() -> None:
t.start()
t.join(timeout=2.0)
- assert not t.is_alive(), (
- "Re-entrant lock acquisition deadlocked -- _opt_lock must be an RLock"
- )
+ assert (
+ not t.is_alive()
+ ), "Re-entrant lock acquisition deadlocked -- _opt_lock must be an RLock"
assert not errors, f"Runner raised: {errors!r}"
assert completed.is_set()
assert harness.exercise_states[0].anim_frame == 7
diff --git a/src/movement_optimizer/tests/test_trajectory_generation.py b/src/movement_optimizer/tests/test_trajectory_generation.py
index ce92e1a950..819c100d5e 100644
--- a/src/movement_optimizer/tests/test_trajectory_generation.py
+++ b/src/movement_optimizer/tests/test_trajectory_generation.py
@@ -96,7 +96,9 @@ def test_via_point_trajectory(self, full_squat_optimizer) -> None:
splines = opt.build_splines(wp.flatten())
q, _, _, _ = opt.eval_trajectory(splines)
mid = len(q) // 2
- assert q[mid, 1] < np.radians(-60), "Thigh should flex significantly at midpoint"
+ assert q[mid, 1] < np.radians(
+ -60
+ ), "Thigh should flex significantly at midpoint"
# ==============================================================
@@ -158,7 +160,9 @@ def test_balance_cost_inside_is_centering_only(self, squat_optimizer) -> None:
opt, body, _, _, _ = squat_optimizer
center = body.inner_center
com_x = np.full(20, center)
- cost = compute_balance_cost(com_x, opt.inner_center, opt.dt, opt.balance_center_weight)
+ cost = compute_balance_cost(
+ com_x, opt.inner_center, opt.dt, opt.balance_center_weight
+ )
# Should be zero since COM == center
assert cost < 1e-10
@@ -194,7 +198,9 @@ def test_total_cost_is_sum(self, squat_optimizer) -> None:
+ compute_endpoint_damping_cost(
qd, qdd, opt.dt, opt.endpoint_weight, opt._n_damp, opt._damp_weights
)
- + compute_balance_cost(com_x, opt.inner_center, opt.dt, opt.balance_center_weight)
+ + compute_balance_cost(
+ com_x, opt.inner_center, opt.dt, opt.balance_center_weight
+ )
)
computed = opt._compute_cost(x)
np.testing.assert_allclose(computed, total, rtol=1e-10)
diff --git a/src/movement_optimizer/tests/test_trajectory_optimization.py b/src/movement_optimizer/tests/test_trajectory_optimization.py
index ef96e3e124..e704147c31 100644
--- a/src/movement_optimizer/tests/test_trajectory_optimization.py
+++ b/src/movement_optimizer/tests/test_trajectory_optimization.py
@@ -51,15 +51,17 @@ def test_precondition_objective_finite(self, squat_optimizer) -> None:
opt, _, _, _, _ = squat_optimizer
wp = opt._initial_guess()
cost = opt._compute_cost(wp.flatten())
- assert cost < float("inf"), "Precondition violated: initial objective is not finite"
+ assert cost < float(
+ "inf"
+ ), "Precondition violated: initial objective is not finite"
def test_postcondition_kkt_within_tol(self, squat_optimizer) -> None:
opt, _, _, _, _ = squat_optimizer
# We assume the optimization result includes 'success' which means KKT conditions are within tolerance
result = opt.optimize()
- assert result.success, (
- "Postcondition violated: optimization did not satisfy KKT within tolerance"
- )
+ assert (
+ result.success
+ ), "Postcondition violated: optimization did not satisfy KKT within tolerance"
def test_cost_decreases(self) -> None:
"""With enough waypoints, optimization should reduce cost."""
@@ -134,12 +136,12 @@ def test_com_stays_in_inner_bos(self) -> None:
)
result = opt.optimize()
com_x = result.com[:, 0]
- assert np.all(com_x >= body.inner_heel - 0.01), (
- f"COM below inner_heel: min={com_x.min():.4f}, bound={body.inner_heel:.4f}"
- )
- assert np.all(com_x <= body.inner_toe + 0.01), (
- f"COM above inner_toe: max={com_x.max():.4f}, bound={body.inner_toe:.4f}"
- )
+ assert np.all(
+ com_x >= body.inner_heel - 0.01
+ ), f"COM below inner_heel: min={com_x.min():.4f}, bound={body.inner_heel:.4f}"
+ assert np.all(
+ com_x <= body.inner_toe + 0.01
+ ), f"COM above inner_toe: max={com_x.max():.4f}, bound={body.inner_toe:.4f}"
assert result.success, "Optimization should report success with COM in bounds"
diff --git a/src/movement_optimizer/tests/test_vector_overlay.py b/src/movement_optimizer/tests/test_vector_overlay.py
index e2ee9855a2..84560a1007 100644
--- a/src/movement_optimizer/tests/test_vector_overlay.py
+++ b/src/movement_optimizer/tests/test_vector_overlay.py
@@ -26,7 +26,9 @@
_MID = _SIZE // 2
-def _flipping_projector(scale: float = 20.0) -> Callable[[tuple[float, float]], QPointF]:
+def _flipping_projector(
+ scale: float = 20.0,
+) -> Callable[[tuple[float, float]], QPointF]:
# Mimics the canvas projector's Y handling: larger world-y -> smaller screen-y.
def _project(point: tuple[float, float]) -> QPointF:
x, y = point
@@ -89,14 +91,18 @@ def test_auto_scale_factor_rejects_nonpositive_target(style: VectorStyle) -> Non
def test_draw_force_arrows_renders_pixels(qapp, style: VectorStyle) -> None:
arrows = [ForceArrow((0.0, 0.0), (1.0, 0.0), style)]
- image = _render(lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=1.0))
+ image = _render(
+ lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=1.0)
+ )
assert _colored_pixels(image)
def test_draw_force_arrows_respects_projector_y_flip(qapp, style: VectorStyle) -> None:
# A +y world vector must render ABOVE the origin (smaller screen-y).
up = [ForceArrow((0.0, 0.0), (0.0, 1.0), style)]
- image = _render(lambda p: draw_force_arrows(p, _flipping_projector(), up, scale=1.0))
+ image = _render(
+ lambda p: draw_force_arrows(p, _flipping_projector(), up, scale=1.0)
+ )
ys = [y for _x, y in _colored_pixels(image)]
assert min(ys) < _MID # reached above the origin row
@@ -104,31 +110,45 @@ def test_draw_force_arrows_respects_projector_y_flip(qapp, style: VectorStyle) -
def test_draw_force_arrows_rejects_nonpositive_scale(qapp, style: VectorStyle) -> None:
arrows = [ForceArrow((0.0, 0.0), (1.0, 0.0), style)]
with pytest.raises(ValueError, match="scale"):
- _render(lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=0.0))
+ _render(
+ lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=0.0)
+ )
def test_draw_force_arrows_rejects_nonfinite_scale(qapp, style: VectorStyle) -> None:
arrows = [ForceArrow((0.0, 0.0), (1.0, 0.0), style)]
with pytest.raises(ValueError, match="scale"):
- _render(lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=float("inf")))
+ _render(
+ lambda p: draw_force_arrows(
+ p, _flipping_projector(), arrows, scale=float("inf")
+ )
+ )
def test_draw_force_arrows_skips_zero_length(qapp, style: VectorStyle) -> None:
arrows = [ForceArrow((0.0, 0.0), (0.0, 0.0), style)]
- image = _render(lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=1.0))
+ image = _render(
+ lambda p: draw_force_arrows(p, _flipping_projector(), arrows, scale=1.0)
+ )
assert not _colored_pixels(image) # no shaft, no head
def test_draw_torque_arcs_renders(qapp, style: VectorStyle) -> None:
arcs = [TorqueArc((0.0, 0.0), 12.0, style), TorqueArc((0.5, 0.0), -8.0, style)]
- image = _render(lambda p: draw_torque_arcs(p, _flipping_projector(), arcs, reference_nm=12.0))
+ image = _render(
+ lambda p: draw_torque_arcs(p, _flipping_projector(), arcs, reference_nm=12.0)
+ )
assert _colored_pixels(image)
-def test_draw_torque_arcs_rejects_nonpositive_reference(qapp, style: VectorStyle) -> None:
+def test_draw_torque_arcs_rejects_nonpositive_reference(
+ qapp, style: VectorStyle
+) -> None:
arcs = [TorqueArc((0.0, 0.0), 1.0, style)]
with pytest.raises(ValueError, match="reference_nm"):
- _render(lambda p: draw_torque_arcs(p, _flipping_projector(), arcs, reference_nm=0.0))
+ _render(
+ lambda p: draw_torque_arcs(p, _flipping_projector(), arcs, reference_nm=0.0)
+ )
def test_draw_com_markers_renders(qapp, style: VectorStyle) -> None:
diff --git a/src/movement_optimizer/theme_bridge.py b/src/movement_optimizer/theme_bridge.py
index 7a318d69bd..ae31e15687 100644
--- a/src/movement_optimizer/theme_bridge.py
+++ b/src/movement_optimizer/theme_bridge.py
@@ -20,8 +20,12 @@
from shared.python.theme import BUILTIN_THEMES as _SHARED_THEMES
from shared.python.theme import ThemedWindowMixin as _SharedThemedWindowMixin
from shared.python.theme import get_theme_manager as _shared_get_theme_manager
- from shared.python.theme.matplotlib_style import apply_plot_theme as _shared_apply_plot_theme
- from shared.python.theme.matplotlib_style import get_chart_color as _shared_get_chart_color
+ from shared.python.theme.matplotlib_style import (
+ apply_plot_theme as _shared_apply_plot_theme,
+ )
+ from shared.python.theme.matplotlib_style import (
+ get_chart_color as _shared_get_chart_color,
+ )
SHARED_THEME_AVAILABLE = True
BUILTIN_THEMES: Mapping[str, Mapping[str, str]] = _SHARED_THEMES
diff --git a/src/movement_optimizer/tool_pack.py b/src/movement_optimizer/tool_pack.py
index eb59011f49..284f45dedf 100644
--- a/src/movement_optimizer/tool_pack.py
+++ b/src/movement_optimizer/tool_pack.py
@@ -42,7 +42,9 @@ def _load_manifest_text() -> str:
repo_manifest = parent / _MANIFEST_FILENAME
if repo_manifest.is_file():
return repo_manifest.read_text(encoding="utf-8")
- raise FileNotFoundError(f"{_MANIFEST_FILENAME} not found alongside movement_optimizer.")
+ raise FileNotFoundError(
+ f"{_MANIFEST_FILENAME} not found alongside movement_optimizer."
+ )
def manifest() -> dict[str, Any]:
diff --git a/src/movement_optimizer/trajectory/optimizer.py b/src/movement_optimizer/trajectory/optimizer.py
index 0caf2b28f5..a251f9783f 100644
--- a/src/movement_optimizer/trajectory/optimizer.py
+++ b/src/movement_optimizer/trajectory/optimizer.py
@@ -129,7 +129,12 @@ def __init__(
self.n_dof = n_dof
self.body, self.dynamics = body, dynamics
self.exercise_type, self.bar_mass = exercise_type, bar_mass
- self.q_start, self.q_end, self.q_bounds, self.q_via = q_start, q_end, q_bounds, q_via
+ self.q_start, self.q_end, self.q_bounds, self.q_via = (
+ q_start,
+ q_end,
+ q_bounds,
+ q_via,
+ )
self.duration, self.n_waypoints, self.n_eval = duration, n_waypoints, n_eval
self.progress_cb, self.n_starts = progress_cb, n_starts
self.cancel_event = cancel_event or threading.Event()
@@ -144,7 +149,9 @@ def __init__(
self.balance_center_weight = BALANCE_CENTER_WEIGHT
self._setup_time_grids()
self.dt = duration / (n_eval - 1)
- self._n_damp = max(ENDPOINT_DAMP_MIN_SAMPLES, int(n_eval * ENDPOINT_DAMP_SAMPLE_FRACTION))
+ self._n_damp = max(
+ ENDPOINT_DAMP_MIN_SAMPLES, int(n_eval * ENDPOINT_DAMP_SAMPLE_FRACTION)
+ )
self._damp_weights = 1.0 - np.arange(self._n_damp) / self._n_damp
self._progress = ProgressTracker(progress_cb=progress_cb)
self._progress_lock = self._progress.lock()
@@ -175,7 +182,9 @@ def build_splines(self, x: NDArray) -> CubicSpline:
self.n_dof,
)
- def eval_trajectory(self, splines: CubicSpline) -> tuple[NDArray, NDArray, NDArray, NDArray]:
+ def eval_trajectory(
+ self, splines: CubicSpline
+ ) -> tuple[NDArray, NDArray, NDArray, NDArray]:
"""Evaluate position, velocity, acceleration, jerk at eval grid.
Delegates to :func:`optimizer_spline.eval_trajectory`.
@@ -345,7 +354,9 @@ def _optimize_single_start(self) -> OptimizationResult:
"""Run single-start path and package its result."""
self._progress.reset()
wp0 = self._initial_guess()
- out = self._minimize_single(wp0.flatten(), self.cost, max_iter=MAX_ITER_PER_START * 2)
+ out = self._minimize_single(
+ wp0.flatten(), self.cost, max_iter=MAX_ITER_PER_START * 2
+ )
if self.cancel_event.is_set():
metrics.increment(
"trajectory_optimization_cancelled_total",
@@ -357,7 +368,9 @@ def _optimize_single_start(self) -> OptimizationResult:
self._record_result_metrics(result, mode="single")
return result
- def _finalize_parallel_results(self, results: list[tuple[Any, int]]) -> OptimizationResult:
+ def _finalize_parallel_results(
+ self, results: list[tuple[Any, int]]
+ ) -> OptimizationResult:
"""Select the best result, log summary, and package output."""
if not results:
raise CancelledError("All optimization starts were cancelled")
@@ -385,11 +398,15 @@ def _record_result_metrics(self, result: OptimizationResult, *, mode: str) -> No
exercise_type=self.exercise_type,
mode=mode,
)
- metrics.observe("trajectory_optimization_elapsed_seconds", result.elapsed_s, **labels)
+ metrics.observe(
+ "trajectory_optimization_elapsed_seconds", result.elapsed_s, **labels
+ )
metrics.observe("trajectory_optimization_cost", result.cost, **labels)
metrics.observe("trajectory_optimization_evaluations", result.n_evals, **labels)
- def _check_solution_feasibility(self, res: Any, q: NDArray, com_x: NDArray) -> tuple[bool, int]:
+ def _check_solution_feasibility(
+ self, res: Any, q: NDArray, com_x: NDArray
+ ) -> tuple[bool, int]:
"""Assess cost finiteness, COM bounds, and joint-limit violations.
SLSQP can report ``success`` while sitting on a point that the
diff --git a/src/movement_optimizer/trajectory/optimizer_cost.py b/src/movement_optimizer/trajectory/optimizer_cost.py
index cd21a5b1a5..2ab1642331 100644
--- a/src/movement_optimizer/trajectory/optimizer_cost.py
+++ b/src/movement_optimizer/trajectory/optimizer_cost.py
@@ -111,7 +111,9 @@ def compute_endpoint_damping_cost(
return weight * float(cost) * dt
-def compute_balance_cost(com_x: NDArray, center: float, dt: float, weight: float) -> float:
+def compute_balance_cost(
+ com_x: NDArray, center: float, dt: float, weight: float
+) -> float:
"""Soft centering preference — penalise COM deviation from the inner BOS center.
Preconditions:
diff --git a/src/movement_optimizer/trajectory/optimizer_parallel.py b/src/movement_optimizer/trajectory/optimizer_parallel.py
index dc4ac3d6c2..2b897cfd4f 100644
--- a/src/movement_optimizer/trajectory/optimizer_parallel.py
+++ b/src/movement_optimizer/trajectory/optimizer_parallel.py
@@ -113,7 +113,9 @@ def run_parallel_starts(
optimizer work performed by each submitted start.
"""
with ThreadPoolExecutor(max_workers=n_workers) as pool:
- pending: set[Future] = {pool.submit(run_single_fn, seed) for seed in range(n_starts)}
+ pending: set[Future] = {
+ pool.submit(run_single_fn, seed) for seed in range(n_starts)
+ }
return collect_future_results(pending, cancel_check, record_progress)
diff --git a/src/p1am_control_system/backend/modbus_client.py b/src/p1am_control_system/backend/modbus_client.py
index 0d5daf4329..15049798fa 100644
--- a/src/p1am_control_system/backend/modbus_client.py
+++ b/src/p1am_control_system/backend/modbus_client.py
@@ -153,7 +153,9 @@ async def read_tags(self) -> dict[str, float] | None:
high = response.registers[i * 2 + 1]
tags[f"TAG_{i}"] = registers_to_float(low, high)
return tags
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception during tag read: {e}")
self._connected = False
return None
@@ -288,7 +290,9 @@ async def write_routing(self, config: RoutingConfig) -> bool:
)
return True
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception writing configuration to PLC: {e}")
self._connected = False
return False
@@ -313,7 +317,9 @@ async def save_to_flash(self) -> bool:
return False
logger.info("Triggered Save to Flash Modbus Coil.")
return True
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception saving config to PLC flash: {e}")
self._connected = False
return False
@@ -359,7 +365,9 @@ async def trigger_estop(self) -> bool:
else:
logger.error("E-stop: one or more zeroing writes FAILED — retry.")
return all_ok
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception during E-stop Modbus execution: {e}")
self._connected = False
return False
@@ -389,7 +397,9 @@ async def clear_estop(self) -> bool:
return False
logger.warning("E-stop reset coil written to PLC successfully.")
return True
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception during E-stop reset Modbus execution: {e}")
self._connected = False
return False
@@ -440,7 +450,9 @@ async def write_pid_setpoint(self, pid_index: int, value: float) -> bool:
value,
resp,
)
- except Exception as exc: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as exc: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(
"write_pid_setpoint(%d, %f) exception: %s",
pid_index,
@@ -497,7 +509,9 @@ async def write_coil(self, address: int, value: bool) -> bool:
if not resp.isError():
return True
logger.error("write_coil(%d, %s) failed: %s", address, value, resp)
- except Exception as exc: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as exc: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(
"write_coil(%d, %s) exception: %s", address, value, exc
)
@@ -546,7 +560,9 @@ async def write_tag(self, tag_name: str, value: float) -> bool:
f"Directly wrote {value} to tag {tag_name} at register {address}."
)
return True
- except Exception as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
+ except (
+ Exception
+ ) as e: # noqa: BLE001 - any I/O failure drops the connection; poll loop reconnects
logger.error(f"Exception during direct tag write for {tag_name}: {e}")
self._connected = False
return False
diff --git a/src/p1am_control_system/desktop/plot_compat.py b/src/p1am_control_system/desktop/plot_compat.py
index dd27fb0768..d3e33c30a7 100644
--- a/src/p1am_control_system/desktop/plot_compat.py
+++ b/src/p1am_control_system/desktop/plot_compat.py
@@ -49,7 +49,9 @@ class _FallbackPyQtGraph:
PlotWidget = _FallbackPlotWidget
@staticmethod
- def mkPen(*args: Any, **kwargs: Any) -> tuple[tuple[Any, ...], dict[str, Any]]: # noqa: N802
+ def mkPen(
+ *args: Any, **kwargs: Any
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]: # noqa: N802
return args, kwargs
pg = _FallbackPyQtGraph()
diff --git a/src/p1am_control_system/desktop/sidebar.py b/src/p1am_control_system/desktop/sidebar.py
index 1e4c78159a..4b5b91ac45 100644
--- a/src/p1am_control_system/desktop/sidebar.py
+++ b/src/p1am_control_system/desktop/sidebar.py
@@ -293,12 +293,12 @@ def _apply_changes(self) -> None:
# Update safety limits
if tag_id < len(self.routing_config.interlocks):
- self.routing_config.interlocks[
- tag_id
- ].low_limit = self.spin_low_limit.value()
- self.routing_config.interlocks[
- tag_id
- ].high_limit = self.spin_high_limit.value()
+ self.routing_config.interlocks[tag_id].low_limit = (
+ self.spin_low_limit.value()
+ )
+ self.routing_config.interlocks[tag_id].high_limit = (
+ self.spin_high_limit.value()
+ )
# Update PID loop configs
if self.pid_group.isVisible() and self.pid_loop_index >= 0:
diff --git a/src/p1am_control_system/frontend/src/components/data_explorer/DataExplorer.tsx b/src/p1am_control_system/frontend/src/components/data_explorer/DataExplorer.tsx
index 2c81130f01..daee5c9414 100644
--- a/src/p1am_control_system/frontend/src/components/data_explorer/DataExplorer.tsx
+++ b/src/p1am_control_system/frontend/src/components/data_explorer/DataExplorer.tsx
@@ -123,7 +123,14 @@ export const DataExplorer: React.FC = ({ triggerNotification
} else {
if (!csv) throw new Error("No CSV loaded");
const n = csv.columns[0]?.values.length ?? 0;
- const index = csv.index ?? Array.from({ length: n }, (_, i) => i);
+ let index = csv.index;
+ if (!index) {
+ // ⚡ Bolt Optimization: Pre-allocate array and populate with a standard for loop to avoid Array.from overhead
+ index = new Array(n);
+ for (let i = 0; i < n; i++) {
+ index[i] = i;
+ }
+ }
req = {
inline: { index, columns: csv.columns },
resample: pipeline.resample ?? undefined,
diff --git a/src/p1am_control_system/frontend/src/lib/explorer/csv.ts b/src/p1am_control_system/frontend/src/lib/explorer/csv.ts
index 8c8562ac8b..37f487137f 100644
--- a/src/p1am_control_system/frontend/src/lib/explorer/csv.ts
+++ b/src/p1am_control_system/frontend/src/lib/explorer/csv.ts
@@ -178,7 +178,11 @@ export function parseCsv(text: string): CsvTable {
// Materialize each source column's raw cells (padding short rows).
// ⚡ Bolt Optimization: Replace dataRows.map() per column with a single-pass loop
- const rawColumns: string[][] = Array.from({ length: colCount }, () => new Array(dataRows.length));
+ // ⚡ Bolt Optimization: Pre-allocate arrays using new Array(N) and standard for loops to avoid Array.from iterability/closure overhead
+ const rawColumns: string[][] = new Array(colCount);
+ for (let c = 0; c < colCount; c += 1) {
+ rawColumns[c] = new Array(dataRows.length);
+ }
for (let r = 0; r < dataRows.length; r += 1) {
const row = dataRows[r];
for (let c = 0; c < colCount; c += 1) {
diff --git a/src/pendulum_simulator/pendulum-core/python/physics_native.py b/src/pendulum_simulator/pendulum-core/python/physics_native.py
index 3f5a7e21df..95fe803be4 100644
--- a/src/pendulum_simulator/pendulum-core/python/physics_native.py
+++ b/src/pendulum_simulator/pendulum-core/python/physics_native.py
@@ -152,7 +152,9 @@ def mass_matrix(self, q: np.ndarray) -> np.ndarray:
raise ValueError(f"q must have shape (2,), got {q.shape}")
if self.use_native:
try:
- result = pendulum_core.py_double_mass_matrix(q.tolist(), self.params.to_rust())
+ result = pendulum_core.py_double_mass_matrix(
+ q.tolist(), self.params.to_rust()
+ )
return _float64_array(result)
except (RuntimeError, AttributeError, TypeError) as e:
logger.warning(
@@ -318,7 +320,9 @@ def __init__(
if not isinstance(val, (int, float)):
raise TypeError(f"{name} must be a number, got {type(val).__name__}")
if not isinstance(m_clubhead, (int, float)):
- raise TypeError(f"m_clubhead must be a number, got {type(m_clubhead).__name__}")
+ raise TypeError(
+ f"m_clubhead must be a number, got {type(m_clubhead).__name__}"
+ )
if m_clubhead < 0:
raise ValueError(f"m_clubhead must be non-negative, got {m_clubhead}")
if not isinstance(g, (int, float)):
@@ -438,7 +442,9 @@ def mass_matrix(self, q: np.ndarray) -> np.ndarray:
raise ValueError(f"q must have shape (8,), got {q.shape}")
if self.use_native:
try:
- result = pendulum_core.py_golfer_mass_matrix(q.tolist(), self.params.to_rust())
+ result = pendulum_core.py_golfer_mass_matrix(
+ q.tolist(), self.params.to_rust()
+ )
return _float64_array(result)
except (RuntimeError, AttributeError, TypeError) as e:
logger.error(
@@ -468,7 +474,9 @@ def gravity_vector(self, q: np.ndarray) -> np.ndarray:
)
return _float64_array(result)
except (RuntimeError, AttributeError, TypeError) as e:
- logger.warning("Rust golfer gravity_vector call failed (%s)", type(e).__name__)
+ logger.warning(
+ "Rust golfer gravity_vector call failed (%s)", type(e).__name__
+ )
# Golfer NumPy fallback is not implemented (see module docstring; native-only, GH#3294).
raise NotImplementedError(
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/__main__.py b/src/pendulum_simulator/src/double_pendulum_golf/__main__.py
index 2862dd911f..08d677b719 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/__main__.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/__main__.py
@@ -30,7 +30,9 @@ class _WheelBlockFilter(QObject):
range and the value survives across launches via QSettings.
"""
- def eventFilter(self, obj: QObject | None, event: QEvent | None) -> bool: # noqa: N802
+ def eventFilter(
+ self, obj: QObject | None, event: QEvent | None
+ ) -> bool: # noqa: N802
if event is not None and event.type() == QEvent.Type.Wheel:
wheel: QWheelEvent = event # type: ignore[assignment]
# Ctrl+Wheel → font zoom (delegated to MainWindow for bounds + persist)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/constraint_solver.py b/src/pendulum_simulator/src/double_pendulum_golf/constraint_solver.py
index 7d15f0bac6..0ef9192513 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/constraint_solver.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/constraint_solver.py
@@ -111,7 +111,9 @@ def _solve_constrained_dynamics(
if not (np.all(np.isfinite(qddot))):
raise ValueError(f"qddot has non-finite values: {qddot}")
if not (np.all(np.isfinite(lambda_forces))):
- raise ValueError(f"Constraint forces have non-finite values: {lambda_forces}")
+ raise ValueError(
+ f"Constraint forces have non-finite values: {lambda_forces}"
+ )
return qddot, lambda_forces
# Compute dynamic terms
@@ -209,7 +211,9 @@ def constraint_forces(
raise ValueError(f"state must have shape ({2 * N_DOF},), got {state.shape}")
if not isinstance(t, (int, float)):
raise TypeError(f"t must be a number, got {type(t).__name__}")
- _, lambda_forces = _solve_constrained_dynamics(state, t, params, torque_func, alpha, beta)
+ _, lambda_forces = _solve_constrained_dynamics(
+ state, t, params, torque_func, alpha, beta
+ )
return lambda_forces
@@ -334,7 +338,9 @@ def project_to_constraints(
if not (tol > 0):
raise ValueError(f"tol must be positive, got {tol}")
- native_projection = _native_backend.golfer_project_to_constraints(q, params, max_iter, tol)
+ native_projection = _native_backend.golfer_project_to_constraints(
+ q, params, max_iter, tol
+ )
if native_projection is not None:
residual = float(np.linalg.norm(constraint_vector(native_projection, params)))
if residual < tol:
@@ -347,7 +353,9 @@ def project_to_constraints(
return q
Phi_q = constraint_jacobian(q, params)
# Use pseudoinverse for robustness
- dq = Phi_q.T @ np.linalg.solve(Phi_q @ Phi_q.T + 1e-12 * np.eye(N_CONSTRAINTS), Phi)
+ dq = Phi_q.T @ np.linalg.solve(
+ Phi_q @ Phi_q.T + 1e-12 * np.eye(N_CONSTRAINTS), Phi
+ )
q -= dq
residual = float(np.linalg.norm(constraint_vector(q, params)))
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/counterfactual.py b/src/pendulum_simulator/src/double_pendulum_golf/counterfactual.py
index c53961f119..d4ae823766 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/counterfactual.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/counterfactual.py
@@ -129,7 +129,9 @@ def zero_torque_joint_forces_double(
# ---------------------------------------------------------------------------
-def _zero_torque_qddot_triple(state: np.ndarray, params: TriplePendulumParams) -> np.ndarray:
+def _zero_torque_qddot_triple(
+ state: np.ndarray, params: TriplePendulumParams
+) -> np.ndarray:
"""Compute angular accel under zero driving torque for triple pendulum.
Preconditions
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/data_extractor.py b/src/pendulum_simulator/src/double_pendulum_golf/data_extractor.py
index 544779cfb7..eddd81ac40 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/data_extractor.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/data_extractor.py
@@ -79,7 +79,9 @@ def _make_velocity_extractor(key: str) -> Extractor:
def _extract(result: Any) -> np.ndarray:
n = result.n_steps
- return np.array([result.joint_velocities_at(i)[key] for i in range(n)], dtype=float)
+ return np.array(
+ [result.joint_velocities_at(i)[key] for i in range(n)], dtype=float
+ )
return _extract
@@ -127,7 +129,9 @@ def _make_base_force_extractor(component: str) -> Extractor:
def _extract(result: Any) -> np.ndarray:
n = result.n_steps
- return np.array([result.base_force_at(i)[component] for i in range(n)], dtype=float)
+ return np.array(
+ [result.base_force_at(i)[component] for i in range(n)], dtype=float
+ )
return _extract
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/dynamics_quantities.py b/src/pendulum_simulator/src/double_pendulum_golf/dynamics_quantities.py
index 5775e936b4..eeb8726d33 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/dynamics_quantities.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/dynamics_quantities.py
@@ -107,7 +107,9 @@ def angular_power_series(
if not (torques.ndim == 1):
raise ValueError(f"torques must be 1-D, got {torques.ndim}-D")
if not (torques.shape == angular_velocities.shape):
- raise ValueError(f"Shape mismatch: {torques.shape} vs {angular_velocities.shape}")
+ raise ValueError(
+ f"Shape mismatch: {torques.shape} vs {angular_velocities.shape}"
+ )
if not (np.all(np.isfinite(torques))):
raise ValueError("torques must be all finite")
if not (np.all(np.isfinite(angular_velocities))):
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/golfer_dynamics.py b/src/pendulum_simulator/src/double_pendulum_golf/golfer_dynamics.py
index 6e1053193b..32430a3af4 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/golfer_dynamics.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/golfer_dynamics.py
@@ -16,7 +16,9 @@
from .physics_golfer import GolferParams, N_DOF, State
-def _mass_point_positions(q: np.ndarray, p: GolferParams) -> list[tuple[float, Callable]]:
+def _mass_point_positions(
+ q: np.ndarray, p: GolferParams
+) -> list[tuple[float, Callable]]:
"""Return list of (mass, position_function) for all point masses."""
if not isinstance(q, np.ndarray):
raise TypeError("q must be a numpy ndarray")
@@ -133,7 +135,9 @@ def __init__(self, q: np.ndarray) -> None:
self.cos_club = np.cos(q[7])
-def _hub_and_shoulder_jacobians(p: GolferParams, tc: _TrigCache) -> dict[str, np.ndarray]:
+def _hub_and_shoulder_jacobians(
+ p: GolferParams, tc: _TrigCache
+) -> dict[str, np.ndarray]:
"""Compute Jacobians for hub, right shoulder, and left shoulder."""
if p is None:
raise ValueError("p must be provided")
@@ -190,7 +194,9 @@ def _right_arm_chain_jacobian(
return J_re, J_rh, J_rh
-def _left_arm_chain_jacobian(p: GolferParams, tc: _TrigCache) -> tuple[np.ndarray, np.ndarray]:
+def _left_arm_chain_jacobian(
+ p: GolferParams, tc: _TrigCache
+) -> tuple[np.ndarray, np.ndarray]:
"""Compute Jacobians for LE, LH along the left arm kinematic chain."""
# LE (left elbow): depends on q[0], q[4]
if p is None:
@@ -482,4 +488,6 @@ def total_energy(state: State, p: GolferParams) -> float:
q = state[:N_DOF]
qdot = state[N_DOF:]
- return total_energy_from_parts(kinetic_energy(q, qdot, p), potential_energy(state, p))
+ return total_energy_from_parts(
+ kinetic_energy(q, qdot, p), potential_energy(state, p)
+ )
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/golfer_kinematics.py b/src/pendulum_simulator/src/double_pendulum_golf/golfer_kinematics.py
index 0a0a6c4145..d37be07b94 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/golfer_kinematics.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/golfer_kinematics.py
@@ -106,7 +106,9 @@ def _absolute_angles(theta_hub: float, relative_angles: list[float]) -> list[flo
return result
-def forward_kinematics(q: np.ndarray, p: GolferParams) -> dict[str, tuple[float, float]]:
+def forward_kinematics(
+ q: np.ndarray, p: GolferParams
+) -> dict[str, tuple[float, float]]:
"""Compute all joint positions in world frame.
Parameters
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/analysis_tab.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/analysis_tab.py
index 97c6add473..11ad32cb05 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/analysis_tab.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/analysis_tab.py
@@ -319,8 +319,12 @@ def _on_plot_2d(self) -> None:
from ..data_extractor import extract_series
try:
- x_vals, x_desc, x_unit = extract_series(self._result, x_key, self._model_type)
- y_vals, y_desc, y_unit = extract_series(self._result, y_key, self._model_type)
+ x_vals, x_desc, x_unit = extract_series(
+ self._result, x_key, self._model_type
+ )
+ y_vals, y_desc, y_unit = extract_series(
+ self._result, y_key, self._model_type
+ )
except (KeyError, AttributeError) as exc:
logger.error("Failed to extract series: %s", exc)
return
@@ -510,7 +514,9 @@ def _evaluator_double(self, z_key: str) -> Any:
if z_key == "potential_energy":
def _eval(angles: dict) -> float:
- state = np.array([angles.get("theta1", 0.0), angles.get("phi", 0.0), 0.0, 0.0])
+ state = np.array(
+ [angles.get("theta1", 0.0), angles.get("phi", 0.0), 0.0, 0.0]
+ )
return potential_energy(state, params)
return _eval
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/base_pendulum_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/base_pendulum_widget.py
index 78f7dffe37..f9d0c4b1c6 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/base_pendulum_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/base_pendulum_widget.py
@@ -402,9 +402,9 @@ def mousePressEvent(self, event: object) -> None:
if not isinstance(event, QMouseEvent):
return
if event.button() == Qt.MouseButton.LeftButton:
- if hasattr(self, "_handle_zoom_button_click") and self._handle_zoom_button_click(
- event.pos()
- ):
+ if hasattr(
+ self, "_handle_zoom_button_click"
+ ) and self._handle_zoom_button_click(event.pos()):
return
self._drag_start = event.pos()
self._drag_pan_start = (self._pan_x, self._pan_y)
@@ -536,7 +536,9 @@ def _world_to_pixel(self, x_world: float, y_world: float) -> QPointF:
# Off-screen detection / recovery overlay
# ------------------------------------------------------------------
- def _world_points_in_view(self, points: list[tuple[float, float]]) -> tuple[bool, QPointF]:
+ def _world_points_in_view(
+ self, points: list[tuple[float, float]]
+ ) -> tuple[bool, QPointF]:
"""Check if any of the given world points lies inside the widget.
Returns ``(any_visible, centroid_pixel)`` where the centroid is
@@ -558,7 +560,9 @@ def _world_points_in_view(self, points: list[tuple[float, float]]) -> tuple[bool
any_visible = True
return any_visible, QPointF(sum_x / n, sum_y / n)
- def _draw_offscreen_indicator(self, painter: QPainter, system_centroid: QPointF) -> None:
+ def _draw_offscreen_indicator(
+ self, painter: QPainter, system_centroid: QPointF
+ ) -> None:
"""Draw a banner + arrow when the system is fully off-screen.
Always-visible recovery affordance: tells the user where to look
@@ -1020,7 +1024,9 @@ def _draw_shadow_projection(
# Image export (#1779)
# ------------------------------------------------------------------
- def export_image(self, file_path: str, width: int = 1920, height: int = 1080) -> None:
+ def export_image(
+ self, file_path: str, width: int = 1920, height: int = 1080
+ ) -> None:
"""Export the current visualization as a high-resolution image.
Supports PNG, SVG, and PDF formats based on file extension.
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/clipboard_utils.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/clipboard_utils.py
index 602164a71c..4ae5fcccad 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/clipboard_utils.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/clipboard_utils.py
@@ -36,7 +36,9 @@ def matrix_to_tsv(data: np.ndarray) -> str:
return result
-def series_to_tsv(x: np.ndarray, y: np.ndarray, x_label: str = "x", y_label: str = "y") -> str:
+def series_to_tsv(
+ x: np.ndarray, y: np.ndarray, x_label: str = "x", y_label: str = "y"
+) -> str:
"""Convert two 1D arrays to tab-separated text with header.
Pre: x.shape == y.shape, both 1D
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_utils.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_utils.py
index 2580b3598f..a65ca0ad1a 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_utils.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_utils.py
@@ -26,7 +26,8 @@
# ── UnitAwareInput availability (DRY: single check shared by all widgets) ──
HAS_UNIT_AWARE_INPUT = (
- importlib.util.find_spec("upstream_drift_tools.ui.widgets.unit_aware_input") is not None
+ importlib.util.find_spec("upstream_drift_tools.ui.widgets.unit_aware_input")
+ is not None
)
# ---------------------------------------------------------------------------
@@ -238,7 +239,9 @@ def parse_coeffs(widget: LabeledInput, name: str) -> list[float]:
parts = widget.value.split(",")
return [float(p.strip()) for p in parts if p.strip()]
except ValueError:
- raise ValueError(f"Cannot parse '{name}' coefficients: '{widget.value}'") from None
+ raise ValueError(
+ f"Cannot parse '{name}' coefficients: '{widget.value}'"
+ ) from None
def parse_coeffs_lenient(widget: LabeledInput) -> list[float]:
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget.py
index e7ac2f5bb8..709d6365a6 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget.py
@@ -274,8 +274,12 @@ def _build_joint_limits_section(self) -> QGroupBox:
self.chk_limits = QCheckBox("Enable joint limits")
self.chk_limits.setStyleSheet(STYLE_CHECK)
layout.addWidget(self.chk_limits)
- self.inp_theta1_min = LabeledInput("θ1 min°", "-180", "Min shoulder angle (deg)", lw)
- self.inp_theta1_max = LabeledInput("θ1 max°", "180", "Max shoulder angle (deg)", lw)
+ self.inp_theta1_min = LabeledInput(
+ "θ1 min°", "-180", "Min shoulder angle (deg)", lw
+ )
+ self.inp_theta1_max = LabeledInput(
+ "θ1 max°", "180", "Max shoulder angle (deg)", lw
+ )
layout.addLayout(_row(self.inp_theta1_min, self.inp_theta1_max))
self.inp_phi_min = LabeledInput("φ min°", "-90", "Min wrist angle (deg)", lw)
self.inp_phi_max = LabeledInput("φ max°", "90", "Max wrist angle (deg)", lw)
@@ -382,7 +386,9 @@ def _build_ic_section(self) -> QGroupBox:
row.addWidget(widget)
layout.addLayout(row)
else:
- self.inp_dtheta1 = LabeledInput("dθ1", "0", "Arm angular velocity rad/s", lw)
+ self.inp_dtheta1 = LabeledInput(
+ "dθ1", "0", "Arm angular velocity rad/s", lw
+ )
self.inp_dphi = LabeledInput("dφ", "0", "Club angular velocity rad/s", lw)
layout.addLayout(_row(self.inp_theta1, self.inp_phi))
layout.addLayout(_row(self.inp_dtheta1, self.inp_dphi))
@@ -394,7 +400,9 @@ def _build_torque_section(self) -> QGroupBox:
layout = QVBoxLayout(box)
layout.setContentsMargins(4, 12, 4, 4)
layout.setSpacing(3)
- self.inp_tau_shoulder = LabeledInput("Shoulder", "-25, 10", "τ(t)=c0+c1·t+…", 56)
+ self.inp_tau_shoulder = LabeledInput(
+ "Shoulder", "-25, 10", "τ(t)=c0+c1·t+…", 56
+ )
self.inp_tau_wrist = LabeledInput("Wrist", "0", "τ(t)=c0+c1·t+…", 56)
layout.addWidget(self.inp_tau_shoulder)
layout.addWidget(self.inp_tau_wrist)
@@ -512,7 +520,9 @@ def _apply_preset(self, name: str) -> None:
raise ValueError("name must be provided")
if name not in self.PRESETS:
return
- theta1, phi, dth, dph, tau_sh, tau_wr, tend, m1, m2, mClub, L1, L2 = self.PRESETS[name]
+ theta1, phi, dth, dph, tau_sh, tau_wr, tend, m1, m2, mClub, L1, L2 = (
+ self.PRESETS[name]
+ )
self.inp_theta1.set_value(str(theta1))
self.inp_phi.set_value(str(phi))
self.inp_tau_shoulder.set_value(tau_sh)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_base.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_base.py
index ccd4d5930e..2ccec6fd30 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_base.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_base.py
@@ -77,9 +77,7 @@
"QPushButton:hover{background:#32326a;}"
)
-STYLE_COMBO = (
- "background:#2a2a38;color:#e0e0f0;border:1px solid #505068;border-radius:3px;padding:4px;"
-)
+STYLE_COMBO = "background:#2a2a38;color:#e0e0f0;border:1px solid #505068;border-radius:3px;padding:4px;"
class ControlsWidgetBase(QWidget):
@@ -269,7 +267,10 @@ def _parse_torque_limits(self) -> list[float] | None:
if not hasattr(self, "chk_clamp") or not self.chk_clamp.isChecked():
return None
- return [parse_float(inp, f"Max torque {i}") for i, inp in enumerate(self.clamp_inputs)]
+ return [
+ parse_float(inp, f"Max torque {i}")
+ for i, inp in enumerate(self.clamp_inputs)
+ ]
def _parse_joint_limits(self) -> tuple[list[float], list[float], float] | None:
"""Parse joint limit values.
@@ -361,7 +362,9 @@ def _on_torque_imported(self, joint: str, coeffs: list[float]) -> None:
inputs = self._get_torque_inputs()
key = joint.lower()
valid_keys = {k.lower() for k in inputs}
- assert key in valid_keys, f"Unknown joint '{joint}', expected one of {valid_keys}"
+ assert (
+ key in valid_keys
+ ), f"Unknown joint '{joint}', expected one of {valid_keys}"
assert len(coeffs) >= 1, "Coefficients list must not be empty"
coeffs_str = ", ".join(f"{c:.4g}" for c in coeffs)
@@ -391,9 +394,9 @@ def set_slider_range(self, max_val: int) -> None:
def set_slider_value(self, val: int) -> None:
"""Pre: 0 <= val <= slider.maximum()"""
- assert 0 <= val <= self.slider.maximum(), (
- f"Slider value {val} out of range [0, {self.slider.maximum()}]"
- )
+ assert (
+ 0 <= val <= self.slider.maximum()
+ ), f"Slider value {val} out of range [0, {self.slider.maximum()}]"
self.slider.blockSignals(True)
self.slider.setValue(val)
self.slider.blockSignals(False)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_golfer.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_golfer.py
index 3eb228dc2d..5da57642d8 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_golfer.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_golfer.py
@@ -220,7 +220,9 @@ def _build_mass_section(self) -> QGroupBox:
row.addWidget(w)
layout.addLayout(row)
else:
- self.inp_m_hub = LabeledInput("Standoff", "0.001", "Standoff mass (massless)")
+ self.inp_m_hub = LabeledInput(
+ "Standoff", "0.001", "Standoff mass (massless)"
+ )
self.inp_m_r_upper = LabeledInput("R Upper", "3.5", "Right upper arm")
self.inp_m_r_fore = LabeledInput("R Fore", "2.0", "Right forearm")
self.inp_m_l_upper = LabeledInput("L Upper", "3.5", "Left upper arm")
@@ -280,7 +282,9 @@ def _build_length_section(self) -> QGroupBox:
row.addWidget(w)
layout.addLayout(row)
else:
- self.inp_L_hub = LabeledInput("Standoff", "0.15", "Standoff length (COM offset)")
+ self.inp_L_hub = LabeledInput(
+ "Standoff", "0.15", "Standoff length (COM offset)"
+ )
self.inp_L_r_upper = LabeledInput("R Upper", "0.35", "Right upper arm")
self.inp_L_r_fore = LabeledInput("R Fore", "0.30", "Right forearm")
self.inp_L_l_upper = LabeledInput("L Upper", "0.35", "Left upper arm")
@@ -303,8 +307,12 @@ def _build_geometry_section(self) -> QGroupBox:
layout = QVBoxLayout(box)
layout.setContentsMargins(4, 12, 4, 4)
layout.setSpacing(3)
- self.inp_d_rs = LabeledInput("d_RS (m)", "0.20", "Hub bar to right shoulder offset")
- self.inp_d_ls = LabeledInput("d_LS (m)", "0.20", "Hub bar to left shoulder offset")
+ self.inp_d_rs = LabeledInput(
+ "d_RS (m)", "0.20", "Hub bar to right shoulder offset"
+ )
+ self.inp_d_ls = LabeledInput(
+ "d_LS (m)", "0.20", "Hub bar to left shoulder offset"
+ )
self.inp_grip_right = LabeledInput(
"Grip R (m)", "0.05", "Right hand grip from club base"
)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_triple.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_triple.py
index 6f62fc37f4..8f330f4bab 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_triple.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/controls_widget_triple.py
@@ -211,8 +211,12 @@ def _build_physics_section(self) -> QGroupBox:
self.inp_L1 = LabeledInput(
"L1 (m) — Hub", "0.20", "Length of segment 1: Hub (sternum → shoulder)"
)
- self.inp_L2 = LabeledInput("L2 (m) — Arm", "0.65", "Length of segment 2: Arm")
- self.inp_L3 = LabeledInput("L3 (m) — Club", "1.10", "Length of segment 3: Club")
+ self.inp_L2 = LabeledInput(
+ "L2 (m) — Arm", "0.65", "Length of segment 2: Arm"
+ )
+ self.inp_L3 = LabeledInput(
+ "L3 (m) — Club", "1.10", "Length of segment 3: Club"
+ )
for w in [
self.inp_m1,
self.inp_m2,
@@ -264,8 +268,12 @@ def _build_torque_section(self) -> QGroupBox:
self.inp_tau_shoulder = LabeledInput(
"Shoulder", "-25, 10", "τ(t) = c0 + c1*t + c2*t^2 + ..."
)
- self.inp_tau_elbow = LabeledInput("Elbow", "0", "τ(t) = c0 + c1*t + c2*t^2 + ...")
- self.inp_tau_wrist = LabeledInput("Wrist", "0", "τ(t) = c0 + c1*t + c2*t^2 + ...")
+ self.inp_tau_elbow = LabeledInput(
+ "Elbow", "0", "τ(t) = c0 + c1*t + c2*t^2 + ..."
+ )
+ self.inp_tau_wrist = LabeledInput(
+ "Wrist", "0", "τ(t) = c0 + c1*t + c2*t^2 + ..."
+ )
layout.addWidget(self.inp_tau_shoulder)
layout.addWidget(self.inp_tau_elbow)
layout.addWidget(self.inp_tau_wrist)
@@ -364,12 +372,24 @@ def get_params(self) -> dict:
L1 = self._uai_or_parse(self.inp_L1, "L1")
L2 = self._uai_or_parse(self.inp_L2, "L2")
L3 = self._uai_or_parse(self.inp_L3, "L3")
- b1 = require_non_negative(parse_float(getattr(self, "inp_b1", None), "b1"), "b1")
- b2 = require_non_negative(parse_float(getattr(self, "inp_b2", None), "b2"), "b2")
- b3 = require_non_negative(parse_float(getattr(self, "inp_b3", None), "b3"), "b3")
- mu1 = require_non_negative(parse_float(getattr(self, "inp_mu1", None), "μ1"), "μ1")
- mu2 = require_non_negative(parse_float(getattr(self, "inp_mu2", None), "μ2"), "μ2")
- mu3 = require_non_negative(parse_float(getattr(self, "inp_mu3", None), "μ3"), "μ3")
+ b1 = require_non_negative(
+ parse_float(getattr(self, "inp_b1", None), "b1"), "b1"
+ )
+ b2 = require_non_negative(
+ parse_float(getattr(self, "inp_b2", None), "b2"), "b2"
+ )
+ b3 = require_non_negative(
+ parse_float(getattr(self, "inp_b3", None), "b3"), "b3"
+ )
+ mu1 = require_non_negative(
+ parse_float(getattr(self, "inp_mu1", None), "μ1"), "μ1"
+ )
+ mu2 = require_non_negative(
+ parse_float(getattr(self, "inp_mu2", None), "μ2"), "μ2"
+ )
+ mu3 = require_non_negative(
+ parse_float(getattr(self, "inp_mu3", None), "μ3"), "μ3"
+ )
require_positive(m1, "m1")
require_positive(m2, "m2")
require_positive(m3, "m3")
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/diagnostics.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/diagnostics.py
index ed22743d43..ccba5b4f5a 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/diagnostics.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/diagnostics.py
@@ -403,9 +403,13 @@ def _populate(self) -> None:
error_count = self._tracker.error_count
total = len(self._tracker.events)
- self._count_label.setText(f"{total} events total • {error_count} errors/critical")
+ self._count_label.setText(
+ f"{total} events total • {error_count} errors/critical"
+ )
- def _on_row_selected(self, row: int, _col: int, _prev_row: int, _prev_col: int) -> None:
+ def _on_row_selected(
+ self, row: int, _col: int, _prev_row: int, _prev_col: int
+ ) -> None:
"""Show details for the selected event."""
# Events are displayed newest-first (reversed)
if 0 <= row < len(self._displayed_events):
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/golfer_pendulum_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/golfer_pendulum_widget.py
index e5e09ba7fc..2c088f0ed8 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/golfer_pendulum_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/golfer_pendulum_widget.py
@@ -326,7 +326,9 @@ def _draw_golfer(self, painter: QPainter) -> None:
self._draw_3d_segment(painter, ls, le, 12, 9, self.COLOR_LEFT_ARM)
self._draw_3d_segment(painter, le, lh, 9, 6, self.COLOR_LEFT_ARM)
# Club shaft — tapered from grip to head
- self._draw_3d_segment(painter, club_base, club_tip, 10, 4, self.COLOR_CLUB_SHAFT)
+ self._draw_3d_segment(
+ painter, club_base, club_tip, 10, 4, self.COLOR_CLUB_SHAFT
+ )
else:
# Original flat-line rendering
# Standoff (origin -> hub) — massless, COM offset adjustment
@@ -542,7 +544,10 @@ def _draw_torque_vectors(self, painter: QPainter) -> None:
for i, jname in enumerate(joint_keys):
if i >= len(torque_list):
break
- if self._visible_segments is not None and jname not in self._visible_segments:
+ if (
+ self._visible_segments is not None
+ and jname not in self._visible_segments
+ ):
continue
jp = pos.get(jname)
if jp is None:
@@ -675,7 +680,10 @@ def _draw_ellipsoids_at_frame(self, painter: QPainter) -> None:
}
for name, ell in data.items():
- if self._visible_segments is not None and name not in self._visible_segments:
+ if (
+ self._visible_segments is not None
+ and name not in self._visible_segments
+ ):
continue
world_pos = endpoint_map.get(name)
if world_pos is None:
@@ -725,7 +733,9 @@ def _draw_ellipsoids_at_frame(self, painter: QPainter) -> None:
QPointF(cx_px + dx_line, cy_px + dy_line),
)
painter.setFont(QFont("Monospace", 7))
- painter.drawText(QPointF(cx_px + dx_line + 4, cy_px + dy_line), "F\u221e")
+ painter.drawText(
+ QPointF(cx_px + dx_line + 4, cy_px + dy_line), "F\u221e"
+ )
def _draw_ellipse_axes(
self,
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/main_window.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/main_window.py
index b902064918..8136da00f9 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/main_window.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/main_window.py
@@ -256,7 +256,9 @@ def adjust_global_font_zoom(cls, delta_steps: int) -> int:
Returns the resulting (clamped) offset.
"""
try:
- current = int(QSettings(_SETTINGS_ORG, _SETTINGS_APP).value("font_zoom_pt", 0))
+ current = int(
+ QSettings(_SETTINGS_ORG, _SETTINGS_APP).value("font_zoom_pt", 0)
+ )
except (TypeError, ValueError):
current = 0
return cls._apply_offset_to_app_font(current + int(delta_steps))
@@ -440,7 +442,9 @@ def _on_shortcut_toggle_3d(self) -> None:
widget = panel.pendulum_widget
new_state = not widget._3d_mode
widget.set_3d_mode(new_state)
- self.statusBar().showMessage(f"3D mode {'enabled' if new_state else 'disabled'}", 2000)
+ self.statusBar().showMessage(
+ f"3D mode {'enabled' if new_state else 'disabled'}", 2000
+ )
def _on_shortcut_toggle_forces(self) -> None:
"""F key: toggle force vector display."""
@@ -448,7 +452,9 @@ def _on_shortcut_toggle_forces(self) -> None:
widget = panel.pendulum_widget
new_state = not widget._show_forces
widget.set_show_forces(new_state)
- self.statusBar().showMessage(f"Forces {'shown' if new_state else 'hidden'}", 2000)
+ self.statusBar().showMessage(
+ f"Forces {'shown' if new_state else 'hidden'}", 2000
+ )
def _on_shortcut_toggle_gravity(self) -> None:
"""G key: toggle gravity display indicator."""
@@ -517,7 +523,9 @@ def _wire_analysis_tab(self) -> None:
for idx, panel in enumerate(self._panels):
model_type = model_map[idx]
- def _on_finished(_p: SimulationPanel = panel, _mt: str = model_type) -> None:
+ def _on_finished(
+ _p: SimulationPanel = panel, _mt: str = model_type
+ ) -> None:
result = _p._result
if result is not None:
self._analysis_tab.set_result(result, model_type=_mt)
@@ -724,7 +732,11 @@ def _on_theme_changed(self, name: str) -> None:
self.status.showMessage(f"Theme changed to: {name}", 3000)
def _open_theme_manager(self) -> None:
- if not _THEME_AVAILABLE or self._theme_manager is None or ThemeManagerDialog is None:
+ if (
+ not _THEME_AVAILABLE
+ or self._theme_manager is None
+ or ThemeManagerDialog is None
+ ):
from PyQt6.QtWidgets import QMessageBox
QMessageBox.information(
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/matrix_widget_base.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/matrix_widget_base.py
index 8eb1dfd9f7..9f399135d1 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/matrix_widget_base.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/matrix_widget_base.py
@@ -114,7 +114,9 @@ def paintEvent(self, event: object) -> None:
if self._result is None:
painter.setPen(self.COLOR_LABEL)
painter.setFont(QFont("Sans", 11))
- painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "No simulation loaded")
+ painter.drawText(
+ self.rect(), Qt.AlignmentFlag.AlignCenter, "No simulation loaded"
+ )
painter.end()
return
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/optimization_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/optimization_widget.py
index 4246abe63e..5208bf4b2a 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/optimization_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/optimization_widget.py
@@ -135,10 +135,14 @@ def _cmaes_step(
# Learning rates
c_sigma = (mu_eff + 2.0) / (n + mu_eff + 5.0)
- d_sigma = 1.0 + 2.0 * max(0.0, math.sqrt((mu_eff - 1.0) / (n + 1.0)) - 1.0) + c_sigma
+ d_sigma = (
+ 1.0 + 2.0 * max(0.0, math.sqrt((mu_eff - 1.0) / (n + 1.0)) - 1.0) + c_sigma
+ )
c_c = (4.0 + mu_eff / n) / (n + 4.0 + 2.0 * mu_eff / n)
c1 = 2.0 / ((n + 1.3) ** 2 + mu_eff)
- c_mu_lr = min(1.0 - c1, 2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((n + 2.0) ** 2 + mu_eff))
+ c_mu_lr = min(
+ 1.0 - c1, 2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((n + 2.0) ** 2 + mu_eff)
+ )
# Sample population
try:
@@ -186,9 +190,9 @@ def _cmaes_step(
else 0.0
)
- p_c_new = (1.0 - c_c) * state.p_c + h_sigma * math.sqrt(c_c * (2.0 - c_c) * mu_eff) * (
- new_mean - old_mean
- ) / state.sigma
+ p_c_new = (1.0 - c_c) * state.p_c + h_sigma * math.sqrt(
+ c_c * (2.0 - c_c) * mu_eff
+ ) * (new_mean - old_mean) / state.sigma
# Update covariance matrix
artmp = (selected - old_mean) / state.sigma
@@ -262,7 +266,9 @@ def __init__(
self._n_iterations = n_iterations
self._method = method
self._warm_start = warm_start
- self._population_size = population_size or max(10, 4 + int(3 * np.log(n_params)))
+ self._population_size = population_size or max(
+ 10, 4 + int(3 * np.log(n_params))
+ )
self._plateau_patience = plateau_patience
self._use_native_batch = use_native_batch
self._native_config = native_batch_config or {}
@@ -333,7 +339,9 @@ def _run_cmaes(self) -> None:
self.finished.emit(
{
"coeffs": (
- state.best_solution if state.best_solution is not None else state.mean
+ state.best_solution
+ if state.best_solution is not None
+ else state.mean
),
"speed": -state.best_fitness,
"history": history,
@@ -491,7 +499,9 @@ def _build_ui_header(self, layout: QVBoxLayout) -> None:
layout.addWidget(title)
backend_lbl = QLabel(
- "[Rust] parallel batch enabled" if _HAS_NATIVE_BATCH else "[Python] sequential"
+ "[Rust] parallel batch enabled"
+ if _HAS_NATIVE_BATCH
+ else "[Python] sequential"
)
backend_lbl.setStyleSheet(
f"color:{'#60c060' if _HAS_NATIVE_BATCH else '#c0a060'};font-size:9px;"
@@ -509,7 +519,9 @@ def _build_ui_config_group(self) -> QGroupBox:
obj_row = QHBoxLayout()
obj_row.addWidget(QLabel("Objective:"))
self._cmb_objective = QComboBox()
- self._cmb_objective.addItems(["Max Tip Speed", "Max Height", "Min Control Effort"])
+ self._cmb_objective.addItems(
+ ["Max Tip Speed", "Max Height", "Min Control Effort"]
+ )
obj_row.addWidget(self._cmb_objective)
cfg_lay.addLayout(obj_row)
@@ -558,7 +570,9 @@ def _build_ui_config_group(self) -> QGroupBox:
self._spin_patience = QSpinBox()
self._spin_patience.setRange(5, 200)
self._spin_patience.setValue(20)
- self._spin_patience.setToolTip("Stop if no improvement for this many generations")
+ self._spin_patience.setToolTip(
+ "Stop if no improvement for this many generations"
+ )
pat_row.addWidget(self._spin_patience)
cfg_lay.addLayout(pat_row)
@@ -698,7 +712,9 @@ def _on_run(self) -> None:
if not self._refresh_bound_objective():
return
if self._objective_fn is None:
- self.append_status_message("⚠ No objective function set. Run a simulation first.")
+ self.append_status_message(
+ "⚠ No objective function set. Run a simulation first."
+ )
return
n_params = self._n_torque_params * self._spin_degree.value()
@@ -716,7 +732,9 @@ def _on_run(self) -> None:
self._log.clear()
self._log.append(f"Starting {method} optimization...")
- self._log.append(f" Params: {n_params}, Generations: {n_iters}, Pop: {pop_size}")
+ self._log.append(
+ f" Params: {n_params}, Generations: {n_iters}, Pop: {pop_size}"
+ )
if _HAS_NATIVE_BATCH and self._chk_native.isChecked():
self._log.append(" Backend: [Rust] parallel (rayon)")
else:
@@ -796,7 +814,9 @@ def _on_finished(self, result: Any) -> None:
if self._convergence_history:
n_gens = len(self._convergence_history)
best = min(self._convergence_history)
- self.append_status_message(f" Generations: {n_gens}, Best loss: {best:.6f}")
+ self.append_status_message(
+ f" Generations: {n_gens}, Best loss: {best:.6f}"
+ )
if coeffs is not None:
self.append_status_message(
@@ -818,4 +838,6 @@ def _on_error(self, msg: str) -> None:
def _on_apply(self) -> None:
if self._result is not None:
self.optimized_coefficients.emit(self._result)
- self.append_status_message("\n✓ Applied optimized coefficients to controls.")
+ self.append_status_message(
+ "\n✓ Applied optimized coefficients to controls."
+ )
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/overlay_state.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/overlay_state.py
index f0be7905e1..41cb6d75b2 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/overlay_state.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/overlay_state.py
@@ -117,7 +117,9 @@ def apply_toolstrip_overlay_state(
for src_attr, dst_setter, extract in _OVERLAY_BINDINGS:
src = getattr(toolstrip, src_attr, None)
if src is None:
- logger.debug("toolstrip has no attribute %r; skipping %s", src_attr, dst_setter)
+ logger.debug(
+ "toolstrip has no attribute %r; skipping %s", src_attr, dst_setter
+ )
continue
setter = getattr(pendulum, dst_setter, None)
if setter is None:
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/panel_builders.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/panel_builders.py
index e3ed5aaff2..c0b6e29bea 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/panel_builders.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/panel_builders.py
@@ -377,7 +377,9 @@ def _wire_panel_sim_signals(
)
# Reset toolstrip play button when playback ends
panel.playback_ended.connect(
- lambda _p=panel: ts.btn_play.setChecked(False) if _p is active_panel_fn() else None
+ lambda _p=panel: (
+ ts.btn_play.setChecked(False) if _p is active_panel_fn() else None
+ )
)
@@ -977,12 +979,18 @@ def _fwd_overlay(attr: str, value: object) -> None:
if hasattr(pw, attr):
getattr(pw, attr)(value)
- ts.torque_vectors_toggled.connect(lambda v: _fwd_overlay("set_show_torque_vectors", v))
- ts.moment_of_force_toggled.connect(lambda v: _fwd_overlay("set_show_moment_of_force", v))
+ ts.torque_vectors_toggled.connect(
+ lambda v: _fwd_overlay("set_show_torque_vectors", v)
+ )
+ ts.moment_of_force_toggled.connect(
+ lambda v: _fwd_overlay("set_show_moment_of_force", v)
+ )
ts.sum_moments_toggled.connect(lambda v: _fwd_overlay("set_show_sum_moments", v))
ts.force_scale_changed.connect(lambda v: _fwd_overlay("set_force_scale", v))
ts.mob_scale_changed.connect(lambda v: _fwd_overlay("set_mob_ellipsoid_scale", v))
- ts.force_ell_scale_changed.connect(lambda v: _fwd_overlay("set_force_ellipsoid_scale", v))
+ ts.force_ell_scale_changed.connect(
+ lambda v: _fwd_overlay("set_force_ellipsoid_scale", v)
+ )
ts.azimuth_changed.connect(lambda v: _fwd_overlay("set_view_azimuth", v))
ts.tilt_changed.connect(lambda v: _fwd_overlay("set_tilt_angle", v))
ts.reset_view_requested.connect(
@@ -1024,7 +1032,9 @@ def wire_toolstrip(main_window: Any) -> None:
)
# ── Simulation action signals → active panel only ──────────────
- ts.run_requested.connect(lambda: main_window._active_panel().controls.run_requested.emit())
+ ts.run_requested.connect(
+ lambda: main_window._active_panel().controls.run_requested.emit()
+ )
ts.reset_requested.connect(
lambda: main_window._active_panel().controls.reset_requested.emit()
)
@@ -1034,7 +1044,9 @@ def wire_toolstrip(main_window: Any) -> None:
ts.speed_changed.connect(
lambda val: main_window._active_panel().controls.speed_changed.emit(val)
)
- ts.frame_scrubbed.connect(lambda idx: main_window._active_panel().scrub_to_frame(idx))
+ ts.frame_scrubbed.connect(
+ lambda idx: main_window._active_panel().scrub_to_frame(idx)
+ )
# ── Export actions (#1141) → active panel's controls ──────────
ts.export_data_requested.connect(
@@ -1121,9 +1133,15 @@ def _fwd_overlay(attr: str, value: object) -> None:
getattr(pw, attr)(value)
ts.forces_toggled.connect(lambda v: _fwd_overlay("set_show_forces", v))
- ts.zero_torque_toggled.connect(lambda v: _fwd_overlay("set_show_zero_torque_forces", v))
- ts.mob_ellipsoid_toggled.connect(lambda v: _fwd_overlay("set_show_mob_ellipsoids", v))
- ts.force_ellipsoid_toggled.connect(lambda v: _fwd_overlay("set_show_force_ellipsoids", v))
+ ts.zero_torque_toggled.connect(
+ lambda v: _fwd_overlay("set_show_zero_torque_forces", v)
+ )
+ ts.mob_ellipsoid_toggled.connect(
+ lambda v: _fwd_overlay("set_show_mob_ellipsoids", v)
+ )
+ ts.force_ellipsoid_toggled.connect(
+ lambda v: _fwd_overlay("set_show_force_ellipsoids", v)
+ )
ts.com_toggled.connect(lambda v: _fwd_overlay("set_show_com", v))
# ── 3D segment rendering (#1155) ──────────────────────────────
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/pendulum_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/pendulum_widget.py
index 9a5ca00b39..d5604f6c49 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/pendulum_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/pendulum_widget.py
@@ -582,7 +582,10 @@ def _draw_torque_vectors(self, painter: QPainter) -> None:
for i, jname in enumerate(joint_names):
if i >= len(torque_list):
break
- if self._visible_segments is not None and jname not in self._visible_segments:
+ if (
+ self._visible_segments is not None
+ and jname not in self._visible_segments
+ ):
continue
jp = pos.get(jname)
if jp is None:
@@ -661,7 +664,10 @@ def _draw_moment_of_force(self, painter: QPainter) -> None:
joint_names.append("wrist")
for jname in joint_names:
- if self._visible_segments is not None and jname not in self._visible_segments:
+ if (
+ self._visible_segments is not None
+ and jname not in self._visible_segments
+ ):
continue
jp = pos.get(jname)
if jp is None:
@@ -738,7 +744,10 @@ def _draw_ellipsoids_at_frame(self, painter: QPainter) -> None:
}
for name, ell in data.items():
- if self._visible_segments is not None and name not in self._visible_segments:
+ if (
+ self._visible_segments is not None
+ and name not in self._visible_segments
+ ):
continue
world_pos = endpoint_map.get(name)
if world_pos is None:
@@ -790,7 +799,9 @@ def _draw_ellipsoids_at_frame(self, painter: QPainter) -> None:
QPointF(cx_px + dx_line, cy_px + dy_line),
)
painter.setFont(QFont("Monospace", 7))
- painter.drawText(QPointF(cx_px + dx_line + 4, cy_px + dy_line), "F∞")
+ painter.drawText(
+ QPointF(cx_px + dx_line + 4, cy_px + dy_line), "F∞"
+ )
def _draw_ellipse_axes(
self,
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/side_panel_tabs.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/side_panel_tabs.py
index 6741c1529d..6a8b843407 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/side_panel_tabs.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/side_panel_tabs.py
@@ -73,7 +73,9 @@ def __init__(
parent: QWidget | None = None,
) -> None:
if not settings_key or not settings_key.strip():
- raise ValueError(f"settings_key must be a non-empty string, got {settings_key!r}")
+ raise ValueError(
+ f"settings_key must be a non-empty string, got {settings_key!r}"
+ )
super().__init__(parent)
self._settings_key: str = settings_key
# Insertion-ordered: label → wrapped scroll area
@@ -124,7 +126,9 @@ def add_panel(
if widget is None:
raise ValueError("widget must not be None")
if label in self._panels:
- raise ValueError(f"duplicate label {label!r} — already used by another panel")
+ raise ValueError(
+ f"duplicate label {label!r} — already used by another panel"
+ )
wrapper = self._wrap(widget)
index = self.addTab(wrapper, label)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel.py
index 1d5f7c10f5..fd1a5cf32a 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel.py
@@ -270,7 +270,9 @@ def _connect_signals(self) -> None:
self.controls.force_scale_changed.connect(self.pendulum.set_force_scale)
# Wire real-time rotation controls (#1146)
- if hasattr(self.controls, "tilt_changed") and hasattr(self.pendulum, "set_tilt_angle"):
+ if hasattr(self.controls, "tilt_changed") and hasattr(
+ self.pendulum, "set_tilt_angle"
+ ):
self.controls.tilt_changed.connect(self.pendulum.set_tilt_angle)
if hasattr(self.controls, "azimuth_changed") and hasattr(
self.pendulum, "set_view_azimuth"
@@ -306,7 +308,9 @@ def _on_run(self) -> None:
p = self.controls.get_params()
except ValueError as e:
logger.warning("Parameter validation failed: %s", e)
- get_tracker().record_exception("simulation", e, context="Parameter validation")
+ get_tracker().record_exception(
+ "simulation", e, context="Parameter validation"
+ )
QMessageBox.warning(self, "Input Error", str(e))
return
@@ -327,7 +331,9 @@ def _on_run(self) -> None:
torque_func = self._torque_builder(p)
except (ValueError, TypeError, KeyError) as e:
logger.warning("State/torque build failed: %s", e, exc_info=True)
- get_tracker().record_exception("simulation", e, context="State/torque build")
+ get_tracker().record_exception(
+ "simulation", e, context="State/torque build"
+ )
QMessageBox.warning(self, "Build Error", str(e))
return
@@ -646,7 +652,9 @@ def _fmt_coeffs(arr: np.ndarray) -> str:
# Triple: split into 3 groups (shoulder, elbow, wrist)
n_third = len(coeffs) // 3
self.controls.inp_tau_shoulder.set_value(_fmt_coeffs(coeffs[:n_third]))
- self.controls.inp_tau_elbow.set_value(_fmt_coeffs(coeffs[n_third : 2 * n_third]))
+ self.controls.inp_tau_elbow.set_value(
+ _fmt_coeffs(coeffs[n_third : 2 * n_third])
+ )
self.controls.inp_tau_wrist.set_value(_fmt_coeffs(coeffs[2 * n_third :]))
logger.info("Applied triple pendulum optimizer coefficients")
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_lifecycle_mixin.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_lifecycle_mixin.py
index 8c8162a12c..829292c4ce 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_lifecycle_mixin.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_lifecycle_mixin.py
@@ -65,7 +65,9 @@ def _on_run(self) -> None:
p = self.controls.get_params()
except ValueError as e:
logger.warning("Parameter validation failed: %s", e)
- get_tracker().record_exception("simulation", e, context="Parameter validation")
+ get_tracker().record_exception(
+ "simulation", e, context="Parameter validation"
+ )
QMessageBox.warning(self, "Input Error", str(e)) # type: ignore[arg-type]
return
@@ -86,7 +88,9 @@ def _on_run(self) -> None:
torque_func = self._torque_builder(p)
except (ValueError, TypeError, KeyError) as e:
logger.warning("State/torque build failed: %s", e, exc_info=True)
- get_tracker().record_exception("simulation", e, context="State/torque build")
+ get_tracker().record_exception(
+ "simulation", e, context="State/torque build"
+ )
QMessageBox.warning(self, "Build Error", str(e)) # type: ignore[arg-type]
return
@@ -277,7 +281,9 @@ def _fmt_coeffs(arr: np.ndarray) -> str:
# Triple: split into 3 groups (shoulder, elbow, wrist)
n_third = len(coeffs) // 3
self.controls.inp_tau_shoulder.set_value(_fmt_coeffs(coeffs[:n_third]))
- self.controls.inp_tau_elbow.set_value(_fmt_coeffs(coeffs[n_third : 2 * n_third]))
+ self.controls.inp_tau_elbow.set_value(
+ _fmt_coeffs(coeffs[n_third : 2 * n_third])
+ )
self.controls.inp_tau_wrist.set_value(_fmt_coeffs(coeffs[2 * n_third :]))
_log.info("Applied triple pendulum optimizer coefficients")
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_simulation_panel.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_simulation_panel.py
index 7339a52a4b..c499874dc0 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_simulation_panel.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/simulation_panel/_simulation_panel.py
@@ -235,7 +235,9 @@ def _connect_signals(self) -> None:
self.controls.force_scale_changed.connect(self.pendulum.set_force_scale)
# Wire real-time rotation controls (#1146)
- if hasattr(self.controls, "tilt_changed") and hasattr(self.pendulum, "set_tilt_angle"):
+ if hasattr(self.controls, "tilt_changed") and hasattr(
+ self.pendulum, "set_tilt_angle"
+ ):
self.controls.tilt_changed.connect(self.pendulum.set_tilt_angle)
if hasattr(self.controls, "azimuth_changed") and hasattr(
self.pendulum, "set_view_azimuth"
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/theme_defaults.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/theme_defaults.py
index 0a0cf95b2d..8cc7a66044 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/theme_defaults.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/theme_defaults.py
@@ -77,7 +77,9 @@ def ensure_default_theme_seeded() -> str:
if not has_initial_flag:
settings.setValue(_INITIAL_FLAG_KEY, "1")
settings.sync()
- active = str(existing_theme) if existing_theme is not None else DEFAULT_THEME_NAME
+ active = (
+ str(existing_theme) if existing_theme is not None else DEFAULT_THEME_NAME
+ )
logger.debug(
"Theme already initialised (theme=%s, flag=%s); not seeding",
active,
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/toolstrip_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/toolstrip_widget.py
index 0fc7373900..93ea6ae256 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/toolstrip_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/toolstrip_widget.py
@@ -38,7 +38,9 @@
# Stylesheet constants
# ---------------------------------------------------------------------------
-_STYLE_STRIP = "QWidget#toolstrip {background: #16162e;border-bottom: 1px solid #2a2a50;}"
+_STYLE_STRIP = (
+ "QWidget#toolstrip {background: #16162e;border-bottom: 1px solid #2a2a50;}"
+)
_BTN_RUN = (
"QPushButton{"
"background:#1e5c30;color:#a8f0b8;border:none;border-radius:5px;"
@@ -203,9 +205,9 @@ def _make_scale_slider(
if style is None:
raise ValueError("style must be provided")
assert divisor > 0, f"divisor must be > 0, got {divisor}"
- assert max_val > 0 and default > 0 and default <= max_val, (
- f"invalid slider bounds: default={default}, max_val={max_val}"
- )
+ assert (
+ max_val > 0 and default > 0 and default <= max_val
+ ), f"invalid slider bounds: default={default}, max_val={max_val}"
s = QSlider(Qt.Orientation.Horizontal)
s.setRange(1, max_val)
s.setValue(default)
@@ -638,7 +640,9 @@ def _build_mobility_ellipsoids_row(self) -> QHBoxLayout:
# Mobility ellipsoids: divisor=100 → raw 1..1000 maps to 0.01×..10×
# so the user can shrink them to 1/100th of unity when joints crowd.
- self._sld_mob = _make_scale_slider(_SLIDER_MOB, default=100, max_val=1000, divisor=100)
+ self._sld_mob = _make_scale_slider(
+ _SLIDER_MOB, default=100, max_val=1000, divisor=100
+ )
self._sld_mob.setToolTip("Mobility ellipsoid display scale (0.01× – 10×)")
self._sld_mob.valueChanged.connect(self._on_mob_scale)
@@ -667,7 +671,9 @@ def _build_force_ellipsoids_row(self) -> QHBoxLayout:
self._lbl_force_ell_scale = QLabel("1.0×")
self._lbl_force_ell_scale.setStyleSheet(_VAL_LBL)
- return _overlay_row(self.chk_force_ell, self._sld_force_ell, self._lbl_force_ell_scale)
+ return _overlay_row(
+ self.chk_force_ell, self._sld_force_ell, self._lbl_force_ell_scale
+ )
def _build_segment_visibility_row(self) -> QHBoxLayout:
"""Row D: Per-segment visibility sub-checkboxes (#1100, #1101, #1102)."""
@@ -919,7 +925,9 @@ def _on_segment_toggled(self) -> None:
If all segments are checked, emit None (show all).
Otherwise emit the set of checked segment names.
"""
- checked = {name for name, chk in self._segment_checks.items() if chk.isChecked()}
+ checked = {
+ name for name, chk in self._segment_checks.items() if chk.isChecked()
+ }
if len(checked) == len(self._segment_checks):
self.segment_visibility_changed.emit(None) # all visible
else:
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_history_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_history_widget.py
index 5a4851cd85..15bf0e8c20 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_history_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_history_widget.py
@@ -161,7 +161,9 @@ def _build_ui(self) -> None:
self._outer_layout.addWidget(title)
if not _HAS_PYQTGRAPH:
- fallback = QLabel("Install pyqtgraph for torque plots:\n pip install pyqtgraph")
+ fallback = QLabel(
+ "Install pyqtgraph for torque plots:\n pip install pyqtgraph"
+ )
fallback.setAlignment(Qt.AlignmentFlag.AlignCenter)
fallback.setStyleSheet("color: #808090; font-size: 11px;")
self._outer_layout.addWidget(fallback)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_preview_widget.py b/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_preview_widget.py
index e66f66eb8a..10832718ea 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_preview_widget.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/gui/torque_preview_widget.py
@@ -45,7 +45,9 @@ def set_profiles(
"""
if profiles is None:
raise ValueError("profiles must be provided")
- self._profiles = [(name, list(coeffs), color) for name, coeffs, color in profiles]
+ self._profiles = [
+ (name, list(coeffs), color) for name, coeffs, color in profiles
+ ]
self._clamp_limits = list(clamp_limits) if clamp_limits else []
self.update()
@@ -62,7 +64,9 @@ def paintEvent(self, event: object) -> None:
if not self._profiles:
painter.setPen(self.COLOR_TEXT)
painter.setFont(QFont("Sans", 9))
- painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "Torque preview")
+ painter.drawText(
+ self.rect(), Qt.AlignmentFlag.AlignCenter, "Torque preview"
+ )
painter.end()
return
@@ -103,7 +107,9 @@ def paintEvent(self, event: object) -> None:
for lv in [limit, -limit]:
y = qrect.bottom() - (lv - v_min) / (v_max - v_min) * qrect.height()
if qrect.top() <= y <= qrect.bottom():
- painter.drawLine(QPointF(qrect.left(), y), QPointF(qrect.right(), y))
+ painter.drawLine(
+ QPointF(qrect.left(), y), QPointF(qrect.right(), y)
+ )
for idx, ((_, values), (__, ___, color)) in enumerate(
zip(series, self._profiles, strict=True)
@@ -117,7 +123,10 @@ def paintEvent(self, event: object) -> None:
points: list[QPointF] = []
for i, val in enumerate(values):
x = qrect.left() + (t[i] / self._t_end) * qrect.width()
- y = qrect.bottom() - (val - v_min) / (v_max - v_min) * qrect.height()
+ y = (
+ qrect.bottom()
+ - (val - v_min) / (v_max - v_min) * qrect.height()
+ )
points.append(QPointF(x, y))
for i in range(1, len(points)):
painter.drawLine(points[i - 1], points[i])
@@ -128,7 +137,10 @@ def paintEvent(self, event: object) -> None:
points = []
for i, val in enumerate(clamped):
x = qrect.left() + (t[i] / self._t_end) * qrect.width()
- y = qrect.bottom() - (val - v_min) / (v_max - v_min) * qrect.height()
+ y = (
+ qrect.bottom()
+ - (val - v_min) / (v_max - v_min) * qrect.height()
+ )
points.append(QPointF(x, y))
for i in range(1, len(points)):
painter.drawLine(points[i - 1], points[i])
@@ -139,7 +151,10 @@ def paintEvent(self, event: object) -> None:
points = []
for i, val in enumerate(values):
x = qrect.left() + (t[i] / self._t_end) * qrect.width()
- y = qrect.bottom() - (val - v_min) / (v_max - v_min) * qrect.height()
+ y = (
+ qrect.bottom()
+ - (val - v_min) / (v_max - v_min) * qrect.height()
+ )
points.append(QPointF(x, y))
for i in range(1, len(points)):
painter.drawLine(points[i - 1], points[i])
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/jacobians_golfer.py b/src/pendulum_simulator/src/double_pendulum_golf/jacobians_golfer.py
index 6d6256fa7d..f1275d58c6 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/jacobians_golfer.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/jacobians_golfer.py
@@ -123,7 +123,9 @@ def delta_matrix(q: np.ndarray, p: GolferParams) -> np.ndarray:
return np.linalg.pinv(M)
-def ztcf_matrix(q: np.ndarray, p: GolferParams, joint_name: str = "club_tip") -> np.ndarray:
+def ztcf_matrix(
+ q: np.ndarray, p: GolferParams, joint_name: str = "club_tip"
+) -> np.ndarray:
"""Compute the Zero-Torque Constraint Force transfer matrix.
Maps applied joint torques to endpoint forces via:
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/joint_moments.py b/src/pendulum_simulator/src/double_pendulum_golf/joint_moments.py
index 5797f53a60..c875896afb 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/joint_moments.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/joint_moments.py
@@ -68,7 +68,9 @@ def moment_of_force(
"""
if joint_position is None:
raise ValueError("joint_position must be provided")
- r = np.asarray(distal_com_position, dtype=float) - np.asarray(joint_position, dtype=float)
+ r = np.asarray(distal_com_position, dtype=float) - np.asarray(
+ joint_position, dtype=float
+ )
return cross_2d(r, np.asarray(net_force, dtype=float))
@@ -139,7 +141,9 @@ def double_pendulum_moments(
# Shoulder: moment about arm COM
m_shoulder = moment_of_force(shoulder, arm_com, f_shoulder)
- total_shoulder = total_moment_at_joint(applied_torques[0], shoulder, arm_com, f_shoulder)
+ total_shoulder = total_moment_at_joint(
+ applied_torques[0], shoulder, arm_com, f_shoulder
+ )
# Wrist: moment about shaft COM
m_wrist = moment_of_force(wrist, shaft_com, f_wrist)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/model_registry.py b/src/pendulum_simulator/src/double_pendulum_golf/model_registry.py
index 8b07ff191d..29cbe096e9 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/model_registry.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/model_registry.py
@@ -88,7 +88,9 @@ def get_model(name: str) -> ModelConfig:
Raises: KeyError if not found.
"""
if name not in _registry:
- raise KeyError(f"Model {name!r} not registered. Available: {list(_registry.keys())}")
+ raise KeyError(
+ f"Model {name!r} not registered. Available: {list(_registry.keys())}"
+ )
return _registry[name]
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/native_backend.py b/src/pendulum_simulator/src/double_pendulum_golf/native_backend.py
index 9b0de07bf5..083ccd4933 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/native_backend.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/native_backend.py
@@ -579,7 +579,9 @@ def golfer_constrained_dynamics(
"""Return native golfer accelerations and multipliers when supported."""
if q is None:
raise ValueError("q must be provided")
- if not golfer_native_enabled() or not golfer_native_constraint_dynamics_supported(params):
+ if not golfer_native_enabled() or not golfer_native_constraint_dynamics_supported(
+ params
+ ):
return None
try:
@@ -693,17 +695,21 @@ def batch_evaluate_double(
"""
if params is None:
raise ValueError("params must be provided")
- if _pendulum_core is None or not hasattr(_pendulum_core, "py_batch_evaluate_double"):
+ if _pendulum_core is None or not hasattr(
+ _pendulum_core, "py_batch_evaluate_double"
+ ):
return None
try:
- result: list[tuple[float, float, bool]] = _pendulum_core.py_batch_evaluate_double(
- _to_rust_double_params(params),
- coeffs_batch,
- n_coeffs_per_joint,
- q0,
- qdot0,
- t_end,
+ result: list[tuple[float, float, bool]] = (
+ _pendulum_core.py_batch_evaluate_double(
+ _to_rust_double_params(params),
+ coeffs_batch,
+ n_coeffs_per_joint,
+ q0,
+ qdot0,
+ t_end,
+ )
)
return result
except (RuntimeError, AttributeError, TypeError) as exc: # pragma: no cover
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/optimizer_gpu.py b/src/pendulum_simulator/src/double_pendulum_golf/optimizer_gpu.py
index 6704a7a918..2b7165f17b 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/optimizer_gpu.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/optimizer_gpu.py
@@ -224,9 +224,9 @@ def loss_fn(coeffs): # type: ignore[no-untyped-def]
logger.info("Iteration %d/%d: loss = %.6f", i + 1, n_iterations, loss_val)
optimal_coeffs = torque_coeffs.reshape(7, n_coeffs_per_joint)
- assert len(history) == n_iterations, (
- f"Expected {n_iterations} history entries, got {len(history)}"
- )
+ assert (
+ len(history) == n_iterations
+ ), f"Expected {n_iterations} history entries, got {len(history)}"
assert optimal_coeffs.shape == (7, n_coeffs_per_joint)
return optimal_coeffs, history
@@ -287,7 +287,9 @@ def optimize_simple_torque_profile(
@jax.jit
@jax.value_and_grad
def loss_fn(coeffs): # type: ignore[no-untyped-def]
- return clubhead_speed_objective(coeffs, params, initial_state, t_end, alpha, beta, dt)
+ return clubhead_speed_objective(
+ coeffs, params, initial_state, t_end, alpha, beta, dt
+ )
history = []
@@ -340,7 +342,9 @@ def compute_gradient_via_finite_difference(
assert eps > 0, f"eps must be positive, got {eps}"
grad = jnp.zeros(7)
- f0 = clubhead_speed_objective(torque_coeffs, params, initial_state, t_end, alpha, beta, dt)
+ f0 = clubhead_speed_objective(
+ torque_coeffs, params, initial_state, t_end, alpha, beta, dt
+ )
for i in range(7):
torque_plus = torque_coeffs.at[i].add(eps) # type: ignore[attr-defined]
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/perturbation_analysis.py b/src/pendulum_simulator/src/double_pendulum_golf/perturbation_analysis.py
index a6f6fb608d..2b465310d1 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/perturbation_analysis.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/perturbation_analysis.py
@@ -110,7 +110,9 @@ def generate_noise(
f"Unknown noise type: {noise_type!r}. Must be 'white', 'pink', or 'brown'."
)
- assert noise.shape == (n_samples,), f"Expected shape ({n_samples},), got {noise.shape}"
+ assert noise.shape == (
+ n_samples,
+ ), f"Expected shape ({n_samples},), got {noise.shape}"
return noise
@@ -150,7 +152,9 @@ def perturb_torque_coeffs(
if not (noise_amplitude >= 0):
raise ValueError("DbC Blocked: Precondition failed.")
if noise_type not in {"white", "pink", "brown"}:
- raise ValueError(f"noise_type must be 'white', 'pink', or 'brown'; got {noise_type!r}")
+ raise ValueError(
+ f"noise_type must be 'white', 'pink', or 'brown'; got {noise_type!r}"
+ )
if noise_amplitude == 0.0:
return [list(c) for c in coeffs]
@@ -196,9 +200,9 @@ class PerturbationConfig:
def __post_init__(self) -> None:
assert self.n_trials > 0, f"n_trials must be positive, got {self.n_trials}"
- assert self.noise_amplitude >= 0, (
- f"noise_amplitude must be non-negative, got {self.noise_amplitude}"
- )
+ assert (
+ self.noise_amplitude >= 0
+ ), f"noise_amplitude must be non-negative, got {self.noise_amplitude}"
assert self.noise_type in {
"white",
"pink",
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/physics.py b/src/pendulum_simulator/src/double_pendulum_golf/physics.py
index 46948b294a..4d0656dac4 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/physics.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/physics.py
@@ -261,7 +261,9 @@ def gravity_vector(theta1: float, phi: float, params: PendulumParams) -> np.ndar
# ---------------------------------------------------------------------------
-def friction_torque_vector(dtheta1: float, dphi: float, params: PendulumParams) -> np.ndarray:
+def friction_torque_vector(
+ dtheta1: float, dphi: float, params: PendulumParams
+) -> np.ndarray:
"""Compute dissipative torque vector (viscous + Coulomb).
Pre: dtheta1, dphi finite.
@@ -494,7 +496,9 @@ def equations_of_motion(
tau_limits = np.zeros(2)
if limits is not None:
- tau_limits = joint_limit_torque(phi, dphi, limits, theta1=theta1, dtheta1=dtheta1)
+ tau_limits = joint_limit_torque(
+ phi, dphi, limits, theta1=theta1, dtheta1=dtheta1
+ )
rhs = tau_drive + tau_friction + tau_limits - C - G
cond = np.linalg.cond(M)
@@ -595,15 +599,28 @@ def base_force(state: State, qddot: np.ndarray, params: PendulumParams) -> dict:
awy = params.L1 * (np.sin(theta1) * qdd1 + np.cos(theta1) * dtheta1**2)
# Tip acceleration (clubhead)
- atx = awx + params.L2 * (np.cos(abs_angle2) * ddabs2 - np.sin(abs_angle2) * dabs2**2)
- aty = awy + params.L2 * (np.sin(abs_angle2) * ddabs2 + np.cos(abs_angle2) * dabs2**2)
+ atx = awx + params.L2 * (
+ np.cos(abs_angle2) * ddabs2 - np.sin(abs_angle2) * dabs2**2
+ )
+ aty = awy + params.L2 * (
+ np.sin(abs_angle2) * ddabs2 + np.cos(abs_angle2) * dabs2**2
+ )
# Shaft COM at L2/2 from wrist
- asx = awx + (params.L2 / 2) * (np.cos(abs_angle2) * ddabs2 - np.sin(abs_angle2) * dabs2**2)
- asy = awy + (params.L2 / 2) * (np.sin(abs_angle2) * ddabs2 + np.cos(abs_angle2) * dabs2**2)
+ asx = awx + (params.L2 / 2) * (
+ np.cos(abs_angle2) * ddabs2 - np.sin(abs_angle2) * dabs2**2
+ )
+ asy = awy + (params.L2 / 2) * (
+ np.sin(abs_angle2) * ddabs2 + np.cos(abs_angle2) * dabs2**2
+ )
fx = params.m1 * ax1 + params.m2 * asx + params.mClub * atx
- fy = params.m1 * ay1 + params.m2 * asy + params.mClub * aty - (params.m1 + me) * params.g
+ fy = (
+ params.m1 * ay1
+ + params.m2 * asy
+ + params.mClub * aty
+ - (params.m1 + me) * params.g
+ )
return {
"fx": float(fx),
@@ -664,7 +681,9 @@ def control_vector(
# ---------------------------------------------------------------------------
-def linear_accelerations(state: State, qddot: np.ndarray, params: PendulumParams) -> dict:
+def linear_accelerations(
+ state: State, qddot: np.ndarray, params: PendulumParams
+) -> dict:
"""Compute linear accelerations of joints in world coordinates."""
if not (state.shape == (4,) and qddot.shape == (2,)):
raise ValueError("state must be (4,) and qddot must be (2,)")
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/physics_golfer_jax.py b/src/pendulum_simulator/src/double_pendulum_golf/physics_golfer_jax.py
index 0e116e286a..6cdb13fdb1 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/physics_golfer_jax.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/physics_golfer_jax.py
@@ -287,8 +287,12 @@ def forward_kinematics_jax(q: JaxArray, p: GolferParamsJAX) -> dict[str, JaxArra
perp_x = jnp.cos(th_hub)
perp_y = jnp.sin(th_hub)
- rs, re, rh = _right_arm_fk_jax(p, hub_x, hub_y, perp_x, perp_y, th_hub, alpha_rs, alpha_re)
- ls, le, lh = _left_arm_fk_jax(p, hub_x, hub_y, perp_x, perp_y, th_hub, alpha_ls, alpha_le)
+ rs, re, rh = _right_arm_fk_jax(
+ p, hub_x, hub_y, perp_x, perp_y, th_hub, alpha_rs, alpha_re
+ )
+ ls, le, lh = _left_arm_fk_jax(
+ p, hub_x, hub_y, perp_x, perp_y, th_hub, alpha_ls, alpha_le
+ )
club_base, grip_left, club_tip = _club_fk_jax(p, rh[0], rh[1], th_club)
return {
@@ -337,18 +341,28 @@ def _right_arm_jacobians_jax(
# RE (Right Elbow): from RS along right upper arm
J_re = jnp.zeros((2, N_DOF))
- J_re = J_re.at[0, 0].set(p.L_hub * cos_hub - p.d_rs * sin_hub + p.L_r_upper * cos_rs)
- J_re = J_re.at[1, 0].set(p.L_hub * sin_hub + p.d_rs * cos_hub + p.L_r_upper * sin_rs)
+ J_re = J_re.at[0, 0].set(
+ p.L_hub * cos_hub - p.d_rs * sin_hub + p.L_r_upper * cos_rs
+ )
+ J_re = J_re.at[1, 0].set(
+ p.L_hub * sin_hub + p.d_rs * cos_hub + p.L_r_upper * sin_rs
+ )
J_re = J_re.at[0, 1].set(p.L_r_upper * cos_rs)
J_re = J_re.at[1, 1].set(p.L_r_upper * sin_rs)
# RH (Right Hand): from RS along right upper + forearm
J_rh = jnp.zeros((2, N_DOF))
J_rh = J_rh.at[0, 0].set(
- p.L_hub * cos_hub - p.d_rs * sin_hub + p.L_r_upper * cos_rs + p.L_r_fore * cos_re
+ p.L_hub * cos_hub
+ - p.d_rs * sin_hub
+ + p.L_r_upper * cos_rs
+ + p.L_r_fore * cos_re
)
J_rh = J_rh.at[1, 0].set(
- p.L_hub * sin_hub + p.d_rs * cos_hub + p.L_r_upper * sin_rs + p.L_r_fore * sin_re
+ p.L_hub * sin_hub
+ + p.d_rs * cos_hub
+ + p.L_r_upper * sin_rs
+ + p.L_r_fore * sin_re
)
J_rh = J_rh.at[0, 1].set(p.L_r_upper * cos_rs + p.L_r_fore * cos_re)
J_rh = J_rh.at[1, 1].set(p.L_r_upper * sin_rs + p.L_r_fore * sin_re)
@@ -379,18 +393,28 @@ def _left_arm_jacobians_jax(
# LE (Left Elbow): from LS along left upper arm
J_le = jnp.zeros((2, N_DOF))
- J_le = J_le.at[0, 0].set(p.L_hub * cos_hub + p.d_ls * sin_hub + p.L_l_upper * cos_ls)
- J_le = J_le.at[1, 0].set(p.L_hub * sin_hub - p.d_ls * cos_hub + p.L_l_upper * sin_ls)
+ J_le = J_le.at[0, 0].set(
+ p.L_hub * cos_hub + p.d_ls * sin_hub + p.L_l_upper * cos_ls
+ )
+ J_le = J_le.at[1, 0].set(
+ p.L_hub * sin_hub - p.d_ls * cos_hub + p.L_l_upper * sin_ls
+ )
J_le = J_le.at[0, 4].set(p.L_l_upper * cos_ls)
J_le = J_le.at[1, 4].set(p.L_l_upper * sin_ls)
# LH (Left Hand): from LS along left upper + forearm
J_lh = jnp.zeros((2, N_DOF))
J_lh = J_lh.at[0, 0].set(
- p.L_hub * cos_hub + p.d_ls * sin_hub + p.L_l_upper * cos_ls + p.L_l_fore * cos_le
+ p.L_hub * cos_hub
+ + p.d_ls * sin_hub
+ + p.L_l_upper * cos_ls
+ + p.L_l_fore * cos_le
)
J_lh = J_lh.at[1, 0].set(
- p.L_hub * sin_hub - p.d_ls * cos_hub + p.L_l_upper * sin_ls + p.L_l_fore * sin_le
+ p.L_hub * sin_hub
+ - p.d_ls * cos_hub
+ + p.L_l_upper * sin_ls
+ + p.L_l_fore * sin_le
)
J_lh = J_lh.at[0, 4].set(p.L_l_upper * cos_ls + p.L_l_fore * cos_le)
J_lh = J_lh.at[1, 4].set(p.L_l_upper * sin_ls + p.L_l_fore * sin_le)
@@ -418,10 +442,16 @@ def _club_jacobians_jax(
"""
# Shared right-hand column values
rh_col0_x = (
- p.L_hub * cos_hub - p.d_rs * sin_hub + p.L_r_upper * cos_rs + p.L_r_fore * cos_re
+ p.L_hub * cos_hub
+ - p.d_rs * sin_hub
+ + p.L_r_upper * cos_rs
+ + p.L_r_fore * cos_re
)
rh_col0_y = (
- p.L_hub * sin_hub + p.d_rs * cos_hub + p.L_r_upper * sin_rs + p.L_r_fore * sin_re
+ p.L_hub * sin_hub
+ + p.d_rs * cos_hub
+ + p.L_r_upper * sin_rs
+ + p.L_r_fore * sin_re
)
rh_col1_x = p.L_r_upper * cos_rs + p.L_r_fore * cos_re
rh_col1_y = p.L_r_upper * sin_rs + p.L_r_fore * sin_re
@@ -492,10 +522,16 @@ def _right_arm_base_jacobian(
J = jnp.zeros((2, N_DOF))
# DOF 0: hub rotation affects the entire chain
J = J.at[0, 0].set(
- p.L_hub * cos_hub - p.d_rs * sin_hub + p.L_r_upper * cos_rs + p.L_r_fore * cos_re
+ p.L_hub * cos_hub
+ - p.d_rs * sin_hub
+ + p.L_r_upper * cos_rs
+ + p.L_r_fore * cos_re
)
J = J.at[1, 0].set(
- p.L_hub * sin_hub + p.d_rs * cos_hub + p.L_r_upper * sin_rs + p.L_r_fore * sin_re
+ p.L_hub * sin_hub
+ + p.d_rs * cos_hub
+ + p.L_r_upper * sin_rs
+ + p.L_r_fore * sin_re
)
# DOF 1: right-shoulder flexion/extension
J = J.at[0, 1].set(p.L_r_upper * cos_rs + p.L_r_fore * cos_re)
@@ -524,10 +560,16 @@ def _left_arm_base_jacobian(
J = jnp.zeros((2, N_DOF))
# DOF 0: hub rotation affects the entire left chain
J = J.at[0, 0].set(
- p.L_hub * cos_hub + p.d_ls * sin_hub + p.L_l_upper * cos_ls + p.L_l_fore * cos_le
+ p.L_hub * cos_hub
+ + p.d_ls * sin_hub
+ + p.L_l_upper * cos_ls
+ + p.L_l_fore * cos_le
)
J = J.at[1, 0].set(
- p.L_hub * sin_hub - p.d_ls * cos_hub + p.L_l_upper * sin_ls + p.L_l_fore * sin_le
+ p.L_hub * sin_hub
+ - p.d_ls * cos_hub
+ + p.L_l_upper * sin_ls
+ + p.L_l_fore * sin_le
)
# DOF 4: left-shoulder flexion/extension
J = J.at[0, 4].set(p.L_l_upper * cos_ls + p.L_l_fore * cos_le)
@@ -664,12 +706,14 @@ def coriolis_jax(q: JaxArray, qdot: JaxArray, p: GolferParamsJAX) -> JaxArray:
M0 = mass_matrix_jax(q, p)
basis = jnp.eye(N_DOF)
- dM = jax.vmap(lambda direction: (mass_matrix_jax(q + eps * direction, p) - M0) / eps)(
- basis
- )
+ dM = jax.vmap(
+ lambda direction: (mass_matrix_jax(q + eps * direction, p) - M0) / eps
+ )(basis)
dM = jnp.transpose(dM, (1, 2, 0))
- christoffel = 0.5 * (dM + jnp.transpose(dM, (0, 2, 1)) - jnp.transpose(dM, (1, 2, 0)))
+ christoffel = 0.5 * (
+ dM + jnp.transpose(dM, (0, 2, 1)) - jnp.transpose(dM, (1, 2, 0))
+ )
return jnp.einsum("ijk,j,k->i", christoffel, qdot, qdot)
@@ -810,7 +854,8 @@ def constraint_jacobian_jax(q: JaxArray, p: GolferParamsJAX) -> JaxArray:
# dPhi[2]/dq: perpendicular distance constraint
Phi_q = Phi_q.at[2, :].set(
- club_perp[0] * (J_lh[0, :] - J_rh[0, :]) + club_perp[1] * (J_lh[1, :] - J_rh[1, :])
+ club_perp[0] * (J_lh[0, :] - J_rh[0, :])
+ + club_perp[1] * (J_lh[1, :] - J_rh[1, :])
)
# d(club_perp)/dq_7: (-sin(th_club), cos(th_club))
d_club_perp_dth = jnp.array([-sin_club, cos_club])
@@ -818,7 +863,8 @@ def constraint_jacobian_jax(q: JaxArray, p: GolferParamsJAX) -> JaxArray:
# dPhi[3]/dq: along-club distance constraint
Phi_q = Phi_q.at[3, :].set(
- club_dir[0] * (J_lh[0, :] - J_rh[0, :]) + club_dir[1] * (J_lh[1, :] - J_rh[1, :])
+ club_dir[0] * (J_lh[0, :] - J_rh[0, :])
+ + club_dir[1] * (J_lh[1, :] - J_rh[1, :])
)
# d(club_dir)/dq_7: (cos(th_club), sin(th_club))
d_club_dir_dth = jnp.array([cos_club, sin_club])
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/physics_triple.py b/src/pendulum_simulator/src/double_pendulum_golf/physics_triple.py
index 3e7c7dec84..54899b41d0 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/physics_triple.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/physics_triple.py
@@ -185,7 +185,9 @@ def mass_matrix(phi1: float, phi2: float, params: TriplePendulumParams) -> np.nd
return M
-def mass_matrix_components(phi1: float, phi2: float, params: TriplePendulumParams) -> dict:
+def mass_matrix_components(
+ phi1: float, phi2: float, params: TriplePendulumParams
+) -> dict:
"""Return individual mass matrix terms with labels.
Returns
@@ -476,7 +478,9 @@ def forward_kinematics(
"""
if theta1 is None:
raise ValueError("theta1 must be provided")
- native_positions = _native_backend.triple_forward_kinematics(theta1, phi1, phi2, params)
+ native_positions = _native_backend.triple_forward_kinematics(
+ theta1, phi1, phi2, params
+ )
if native_positions is not None:
return native_positions
@@ -559,7 +563,9 @@ def linear_accelerations(
}
-def net_joint_forces(state: State, qddot: np.ndarray, params: TriplePendulumParams) -> dict:
+def net_joint_forces(
+ state: State, qddot: np.ndarray, params: TriplePendulumParams
+) -> dict:
"""Compute net joint forces (proximal on distal) in world coordinates.
Returns
@@ -621,7 +627,9 @@ def potential_energy(state: State, params: TriplePendulumParams) -> float:
V = (
-m1 * g * L1 * np.cos(theta1)
- m2 * g * (L1 * np.cos(theta1) + L2 * np.cos(abs_angle2))
- - m3 * g * (L1 * np.cos(theta1) + L2 * np.cos(abs_angle2) + L3 * np.cos(abs_angle3))
+ - m3
+ * g
+ * (L1 * np.cos(theta1) + L2 * np.cos(abs_angle2) + L3 * np.cos(abs_angle3))
)
return float(V)
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/simulation.py b/src/pendulum_simulator/src/double_pendulum_golf/simulation.py
index 4cead3b042..88bfe3c629 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/simulation.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/simulation.py
@@ -224,7 +224,9 @@ def run_simulation(
qdot0 = initial_state[2:4].tolist()
t_span = (0.0, t_end)
max_steps = int(max(t_end / dt * 10, 100000))
- res = simulate_double(params, q0, qdot0, coeffs, n_coeffs_per_joint, t_span, max_steps)
+ res = simulate_double(
+ params, q0, qdot0, coeffs, n_coeffs_per_joint, t_span, max_steps
+ )
if res is not None:
t_res, states_res = res
if len(t_res) >= 2:
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/simulation_golfer.py b/src/pendulum_simulator/src/double_pendulum_golf/simulation_golfer.py
index 734853c7d7..bca4f507aa 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/simulation_golfer.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/simulation_golfer.py
@@ -98,7 +98,9 @@ def positions_at(self, idx: int) -> dict:
self._check_idx(idx)
return forward_kinematics(self.q_at(idx), self.params) # type: ignore[no-any-return]
- def torques_at(self, idx: int) -> tuple[float, float, float, float, float, float, float]:
+ def torques_at(
+ self, idx: int
+ ) -> tuple[float, float, float, float, float, float, float]:
"""Applied driving torques at time index."""
if idx is None:
raise ValueError("idx must be provided")
@@ -129,7 +131,9 @@ def constraint_forces_at(self, idx: int) -> np.ndarray:
if idx is None:
raise ValueError("idx must be provided")
self._check_idx(idx)
- return constraint_forces(self.states[idx], self.t[idx], self.params, self.torque_func)
+ return constraint_forces(
+ self.states[idx], self.t[idx], self.params, self.torque_func
+ )
def constraint_violation_at(self, idx: int) -> float:
"""Constraint violation magnitude at time index."""
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/simulation_result_base.py b/src/pendulum_simulator/src/double_pendulum_golf/simulation_result_base.py
index 29459e5f02..15dbc08896 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/simulation_result_base.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/simulation_result_base.py
@@ -77,13 +77,17 @@ def all_energies(self) -> dict[str, np.ndarray]:
energy_at = getattr(self, "energy_at")
first = energy_at(0)
return {
- key: np.asarray([energy_at(i)[key] for i in range(self.n_steps)], dtype=float)
+ key: np.asarray(
+ [energy_at(i)[key] for i in range(self.n_steps)], dtype=float
+ )
for key in first
}
def all_accelerations(self) -> np.ndarray:
accelerations_at = getattr(self, "accelerations_at")
- return np.asarray([accelerations_at(i) for i in range(self.n_steps)], dtype=float)
+ return np.asarray(
+ [accelerations_at(i) for i in range(self.n_steps)], dtype=float
+ )
def all_torques(self) -> np.ndarray:
torques_at = getattr(self, "torques_at")
diff --git a/src/pendulum_simulator/src/double_pendulum_golf/torque_utils.py b/src/pendulum_simulator/src/double_pendulum_golf/torque_utils.py
index 5a3f5726bd..cc3813440c 100644
--- a/src/pendulum_simulator/src/double_pendulum_golf/torque_utils.py
+++ b/src/pendulum_simulator/src/double_pendulum_golf/torque_utils.py
@@ -56,7 +56,9 @@ def make_polynomial_torque(
polys: list[np.ndarray] = []
for i, coeffs in enumerate(coeffs_per_joint):
if not (len(coeffs) >= 1):
- raise ValueError(f"Need at least one coefficient for joint {i}, got {len(coeffs)}")
+ raise ValueError(
+ f"Need at least one coefficient for joint {i}, got {len(coeffs)}"
+ )
# Reverse: our convention is [c0, c1, c2, ...] (ascending),
# np.polyval expects [cN, ..., c1, c0] (descending).
polys.append(np.array(coeffs[::-1]))
diff --git a/src/pendulum_simulator/tests/test_analysis_tab.py b/src/pendulum_simulator/tests/test_analysis_tab.py
index ead705920b..6a4f99748b 100644
--- a/src/pendulum_simulator/tests/test_analysis_tab.py
+++ b/src/pendulum_simulator/tests/test_analysis_tab.py
@@ -30,7 +30,9 @@ def test_det_of_identity(self) -> None:
def test_det_of_known_matrix(self) -> None:
"""det([[2,0],[0,3]]) = 6.0."""
- evaluator = _make_det_evaluator(lambda angles: np.array([[2.0, 0.0], [0.0, 3.0]]))
+ evaluator = _make_det_evaluator(
+ lambda angles: np.array([[2.0, 0.0], [0.0, 3.0]])
+ )
assert evaluator({}) == pytest.approx(6.0)
def test_det_passes_angles_to_fn(self) -> None:
@@ -56,7 +58,9 @@ def test_cond_of_identity(self) -> None:
def test_cond_of_diagonal(self) -> None:
"""cond(diag(1, 10)) = 10.0."""
- evaluator = _make_cond_evaluator(lambda angles: np.array([[1.0, 0.0], [0.0, 10.0]]))
+ evaluator = _make_cond_evaluator(
+ lambda angles: np.array([[1.0, 0.0], [0.0, 10.0]])
+ )
assert evaluator({}) == pytest.approx(10.0, rel=1e-6)
def test_cond_passes_angles_to_fn(self) -> None:
@@ -253,5 +257,7 @@ def test_analysis_tab_plot_2d_errors(qapp, monkeypatch) -> Any:
def mock_extract(*args) -> Any:
raise KeyError()
- monkeypatch.setattr("double_pendulum_golf.data_extractor.extract_series", mock_extract)
+ monkeypatch.setattr(
+ "double_pendulum_golf.data_extractor.extract_series", mock_extract
+ )
tab._on_plot_2d()
diff --git a/src/pendulum_simulator/tests/test_analytical_jacobians.py b/src/pendulum_simulator/tests/test_analytical_jacobians.py
index 500c249e93..b5f0ad9e95 100644
--- a/src/pendulum_simulator/tests/test_analytical_jacobians.py
+++ b/src/pendulum_simulator/tests/test_analytical_jacobians.py
@@ -101,9 +101,9 @@ def hub_pos(qq):
J_numerical = _numerical_jacobian_point(hub_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"Hub Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"Hub Jacobian mismatch at q={q}"
def test_re_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
"""RE Jacobian (depends on q[0], q[1])."""
@@ -118,9 +118,9 @@ def re_pos(qq):
J_numerical = _numerical_jacobian_point(re_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"RE Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"RE Jacobian mismatch at q={q}"
def test_rh_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
"""RH Jacobian (depends on q[0], q[1], q[2])."""
@@ -135,9 +135,9 @@ def rh_pos(qq):
J_numerical = _numerical_jacobian_point(rh_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"RH Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"RH Jacobian mismatch at q={q}"
def test_le_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
"""LE Jacobian (depends on q[0], q[4])."""
@@ -152,9 +152,9 @@ def le_pos(qq):
J_numerical = _numerical_jacobian_point(le_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"LE Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"LE Jacobian mismatch at q={q}"
def test_lh_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
"""LH Jacobian (depends on q[0], q[4], q[5])."""
@@ -169,11 +169,13 @@ def lh_pos(qq):
J_numerical = _numerical_jacobian_point(lh_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"LH Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"LH Jacobian mismatch at q={q}"
- def test_club_com_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
+ def test_club_com_jacobian_vs_numerical(
+ self, test_configs: list[np.ndarray]
+ ) -> None:
"""Club COM Jacobian (depends on q[0], q[1], q[2], q[3], q[7])."""
from double_pendulum_golf.physics_golfer import analytical_fk_jacobians
@@ -188,11 +190,13 @@ def club_com_pos(qq):
J_numerical = _numerical_jacobian_point(club_com_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"Club COM Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"Club COM Jacobian mismatch at q={q}"
- def test_club_tip_jacobian_vs_numerical(self, test_configs: list[np.ndarray]) -> None:
+ def test_club_tip_jacobian_vs_numerical(
+ self, test_configs: list[np.ndarray]
+ ) -> None:
"""Club tip Jacobian (depends on q[0], q[1], q[2], q[3], q[7])."""
from double_pendulum_golf.physics_golfer import analytical_fk_jacobians
@@ -205,9 +209,9 @@ def club_tip_pos(qq):
J_numerical = _numerical_jacobian_point(club_tip_pos, q)
- assert np.allclose(J_analytical, J_numerical, atol=1e-5, rtol=1e-4), (
- f"Club tip Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ J_analytical, J_numerical, atol=1e-5, rtol=1e-4
+ ), f"Club tip Jacobian mismatch at q={q}"
class TestAnalyticalMassMatrix:
@@ -220,7 +224,9 @@ def test_module_exports_analytical_mass_matrix(self) -> None:
assert hasattr(physics_golfer, "analytical_mass_matrix")
assert callable(physics_golfer.analytical_mass_matrix)
- def test_analytical_mass_matrix_parity(self, test_configs: list[np.ndarray]) -> None:
+ def test_analytical_mass_matrix_parity(
+ self, test_configs: list[np.ndarray]
+ ) -> None:
"""Analytical mass matrix matches numerical at 20 configs."""
from double_pendulum_golf.physics_golfer import analytical_mass_matrix
@@ -228,9 +234,9 @@ def test_analytical_mass_matrix_parity(self, test_configs: list[np.ndarray]) ->
M_analytical = analytical_mass_matrix(q, _PARAMS)
M_numerical = numerical_mass_matrix(q, _PARAMS)
- assert np.allclose(M_analytical, M_numerical, atol=1e-6, rtol=1e-4), (
- f"Mass matrix mismatch at q={q}"
- )
+ assert np.allclose(
+ M_analytical, M_numerical, atol=1e-6, rtol=1e-4
+ ), f"Mass matrix mismatch at q={q}"
def test_mass_matrix_symmetric(self, test_configs: list[np.ndarray]) -> None:
"""Analytical mass matrix is symmetric."""
@@ -270,11 +276,13 @@ def test_analytical_coriolis_parity(self, test_configs: list[np.ndarray]) -> Non
C_analytical = analytical_coriolis(q, qdot, _PARAMS)
C_numerical = numerical_coriolis(q, qdot, _PARAMS)
- assert np.allclose(C_analytical, C_numerical, atol=1e-5, rtol=1e-3), (
- f"Coriolis mismatch at q={q}, qdot={qdot}"
- )
+ assert np.allclose(
+ C_analytical, C_numerical, atol=1e-5, rtol=1e-3
+ ), f"Coriolis mismatch at q={q}, qdot={qdot}"
- def test_coriolis_zero_at_zero_velocity(self, test_configs: list[np.ndarray]) -> None:
+ def test_coriolis_zero_at_zero_velocity(
+ self, test_configs: list[np.ndarray]
+ ) -> None:
"""Coriolis is zero when velocity is zero."""
from double_pendulum_golf.physics_golfer import analytical_coriolis
@@ -302,9 +310,9 @@ def test_analytical_gravity_parity(self, test_configs: list[np.ndarray]) -> None
G_analytical = analytical_gravity_vector(q, _PARAMS)
G_numerical = numerical_gravity(q, _PARAMS)
- assert np.allclose(G_analytical, G_numerical, atol=1e-5, rtol=1e-4), (
- f"Gravity mismatch at q={q}"
- )
+ assert np.allclose(
+ G_analytical, G_numerical, atol=1e-5, rtol=1e-4
+ ), f"Gravity mismatch at q={q}"
class TestAnalyticalConstraintJacobian:
@@ -317,7 +325,9 @@ def test_module_exports_analytical_constraint_jac(self) -> None:
assert hasattr(physics_golfer, "analytical_constraint_jacobian")
assert callable(physics_golfer.analytical_constraint_jacobian)
- def test_analytical_constraint_jac_parity(self, test_configs: list[np.ndarray]) -> None:
+ def test_analytical_constraint_jac_parity(
+ self, test_configs: list[np.ndarray]
+ ) -> None:
"""Analytical constraint Jacobian matches numerical at 20 configs."""
from double_pendulum_golf.physics_golfer import (
analytical_constraint_jacobian,
@@ -327,9 +337,9 @@ def test_analytical_constraint_jac_parity(self, test_configs: list[np.ndarray])
Phi_q_analytical = analytical_constraint_jacobian(q, _PARAMS)
Phi_q_numerical = numerical_constraint_jac(q, _PARAMS)
- assert np.allclose(Phi_q_analytical, Phi_q_numerical, atol=1e-5, rtol=1e-4), (
- f"Constraint Jacobian mismatch at q={q}"
- )
+ assert np.allclose(
+ Phi_q_analytical, Phi_q_numerical, atol=1e-5, rtol=1e-4
+ ), f"Constraint Jacobian mismatch at q={q}"
def test_constraint_jac_shape(self) -> None:
"""Constraint Jacobian has shape (4, 8)."""
@@ -364,9 +374,9 @@ def test_analytical_bias_parity(self, test_configs: list[np.ndarray]) -> None:
gamma_analytical = analytical_constraint_acceleration_bias(q, qdot, _PARAMS)
gamma_numerical = numerical_bias(q, qdot, _PARAMS)
- assert np.allclose(gamma_analytical, gamma_numerical, atol=1e-5, rtol=1e-3), (
- f"Bias mismatch at q={q}, qdot={qdot}"
- )
+ assert np.allclose(
+ gamma_analytical, gamma_numerical, atol=1e-5, rtol=1e-3
+ ), f"Bias mismatch at q={q}, qdot={qdot}"
def test_bias_zero_at_zero_velocity(self, test_configs: list[np.ndarray]) -> None:
"""Bias is zero when velocity is zero."""
diff --git a/src/pendulum_simulator/tests/test_club_forces.py b/src/pendulum_simulator/tests/test_club_forces.py
index f3c43d7e34..11ff2c37bb 100644
--- a/src/pendulum_simulator/tests/test_club_forces.py
+++ b/src/pendulum_simulator/tests/test_club_forces.py
@@ -370,7 +370,9 @@ def test_delta_zero_torque_zero_forces(self, default_params):
# So F = m*0 - m*(0, -g) = (0, m*g)
# Net force should be +(m_rh + m_lh)*g in the y direction
net_fy = result["net_force"][1]
- expected_fy = (default_params.m_r_fore + default_params.m_l_fore) * default_params.g
+ expected_fy = (
+ default_params.m_r_fore + default_params.m_l_fore
+ ) * default_params.g
assert net_fy == pytest.approx(expected_fy, rel=0.01)
@@ -431,16 +433,22 @@ def test_delta_state_wrong_type(self, default_params):
from double_pendulum_golf.club_forces import delta_club_decomposition
with pytest.raises(TypeError, match="state must be a numpy ndarray"):
- delta_club_decomposition(state=list(range(16)), tau=np.zeros(8), p=default_params)
+ delta_club_decomposition(
+ state=list(range(16)), tau=np.zeros(8), p=default_params
+ )
def test_delta_tau_wrong_type(self, default_params):
from double_pendulum_golf.club_forces import delta_club_decomposition
with pytest.raises(TypeError, match="tau must be a numpy ndarray"):
- delta_club_decomposition(state=np.zeros(16), tau=[0.0] * 8, p=default_params)
+ delta_club_decomposition(
+ state=np.zeros(16), tau=[0.0] * 8, p=default_params
+ )
def test_delta_tau_wrong_shape(self, default_params):
from double_pendulum_golf.club_forces import delta_club_decomposition
with pytest.raises(ValueError, match="tau must have shape"):
- delta_club_decomposition(state=np.zeros(16), tau=np.zeros(4), p=default_params)
+ delta_club_decomposition(
+ state=np.zeros(16), tau=np.zeros(4), p=default_params
+ )
diff --git a/src/pendulum_simulator/tests/test_club_forces_extended.py b/src/pendulum_simulator/tests/test_club_forces_extended.py
index 035e8ce763..b047bba2fd 100644
--- a/src/pendulum_simulator/tests/test_club_forces_extended.py
+++ b/src/pendulum_simulator/tests/test_club_forces_extended.py
@@ -66,7 +66,9 @@ class TestOverallClubDecomposition:
Uses real constrained dynamics with zero torques — simplest valid case.
"""
- def test_returns_required_keys(self, params: GolferParams, zero_state: np.ndarray) -> None:
+ def test_returns_required_keys(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
result = overall_club_decomposition(zero_state, 0.0, params, zero_torque)
for key in (
"net_force",
@@ -78,7 +80,9 @@ def test_returns_required_keys(self, params: GolferParams, zero_state: np.ndarra
):
assert key in result, f"Missing key: {key}"
- def test_net_force_is_array(self, params: GolferParams, zero_state: np.ndarray) -> None:
+ def test_net_force_is_array(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
result = overall_club_decomposition(zero_state, 0.0, params, zero_torque)
assert isinstance(result["net_force"], np.ndarray)
assert result["net_force"].shape == (2,)
@@ -89,11 +93,15 @@ def test_action_point_is_finite(
result = overall_club_decomposition(zero_state, 0.0, params, zero_torque)
assert np.all(np.isfinite(result["action_point"]))
- def test_couple_is_finite(self, params: GolferParams, zero_state: np.ndarray) -> None:
+ def test_couple_is_finite(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
result = overall_club_decomposition(zero_state, 0.0, params, zero_torque)
assert np.isfinite(result["couple"])
- def test_all_values_finite(self, params: GolferParams, zero_state: np.ndarray) -> None:
+ def test_all_values_finite(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
result = overall_club_decomposition(zero_state, 0.0, params, zero_torque)
for key, val in result.items():
if isinstance(val, np.ndarray):
@@ -103,15 +111,25 @@ def test_all_values_finite(self, params: GolferParams, zero_state: np.ndarray) -
def test_alpha_midpoint(self, params: GolferParams, zero_state: np.ndarray) -> None:
"""alpha=0 gives midpoint between grip positions."""
- result = overall_club_decomposition(zero_state, 0.0, params, zero_torque, alpha=0.0)
+ result = overall_club_decomposition(
+ zero_state, 0.0, params, zero_torque, alpha=0.0
+ )
assert result["action_point"].shape == (2,)
- def test_alpha_right_grip(self, params: GolferParams, zero_state: np.ndarray) -> None:
- result = overall_club_decomposition(zero_state, 0.0, params, zero_torque, alpha=-1.0)
+ def test_alpha_right_grip(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
+ result = overall_club_decomposition(
+ zero_state, 0.0, params, zero_torque, alpha=-1.0
+ )
assert all(np.isfinite(result["action_point"]))
- def test_alpha_left_grip(self, params: GolferParams, zero_state: np.ndarray) -> None:
- result = overall_club_decomposition(zero_state, 0.0, params, zero_torque, alpha=1.0)
+ def test_alpha_left_grip(
+ self, params: GolferParams, zero_state: np.ndarray
+ ) -> None:
+ result = overall_club_decomposition(
+ zero_state, 0.0, params, zero_torque, alpha=1.0
+ )
assert all(np.isfinite(result["action_point"]))
@@ -181,7 +199,9 @@ def test_applied_torques_preserved(
)
joints = ["hub", "rs", "re", "rh", "ls", "le", "lh"]
for i, joint in enumerate(joints):
- assert result[f"{joint}_applied_torque"] == pytest.approx(applied_torques[i])
+ assert result[f"{joint}_applied_torque"] == pytest.approx(
+ applied_torques[i]
+ )
def test_all_values_finite(
self, full_positions: dict, full_forces: dict, applied_torques: tuple
@@ -218,15 +238,21 @@ def test_fewer_than_7_torques_raises(
self, full_positions: dict, full_forces: dict
) -> None:
with pytest.raises((ValueError, TypeError, AssertionError), match="Need >= 7"):
- golfer_pendulum_moments(full_positions, full_forces, (1.0, 2.0, 3.0), object())
+ golfer_pendulum_moments(
+ full_positions, full_forces, (1.0, 2.0, 3.0), object()
+ )
- def test_exactly_7_torques_ok(self, full_positions: dict, full_forces: dict) -> None:
+ def test_exactly_7_torques_ok(
+ self, full_positions: dict, full_forces: dict
+ ) -> None:
torques = (1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0)
result = golfer_pendulum_moments(full_positions, full_forces, torques, object())
assert len(result) == 21
def test_zero_forces_moment_of_force_is_zero(self, full_positions: dict) -> None:
- forces = {joint: (0.0, 0.0) for joint in ("hub", "rs", "re", "rh", "ls", "le", "lh")}
+ forces = {
+ joint: (0.0, 0.0) for joint in ("hub", "rs", "re", "rh", "ls", "le", "lh")
+ }
torques = (1.0,) * 7
result = golfer_pendulum_moments(full_positions, forces, torques, object())
for joint in ("hub", "rs", "re", "rh", "ls", "le", "lh"):
diff --git a/src/pendulum_simulator/tests/test_constraint_solver.py b/src/pendulum_simulator/tests/test_constraint_solver.py
index c61c15ff71..7e46f97b85 100644
--- a/src/pendulum_simulator/tests/test_constraint_solver.py
+++ b/src/pendulum_simulator/tests/test_constraint_solver.py
@@ -56,7 +56,9 @@ def golfer_params() -> GolferParams:
@pytest.fixture
-def zero_torque() -> Callable[[float], tuple[float, float, float, float, float, float, float]]:
+def zero_torque() -> (
+ Callable[[float], tuple[float, float, float, float, float, float, float]]
+):
"""Zero torque function for all joints."""
return lambda t: (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
@@ -77,18 +79,18 @@ def test_zero_config_projects(self, golfer_params: GolferParams) -> None:
q = np.zeros(N_DOF)
q_proj = project_to_constraints(q, golfer_params)
phi = constraint_vector(q_proj, golfer_params)
- assert np.linalg.norm(phi) < 1e-6, (
- f"Constraint violation after projection: {np.linalg.norm(phi)}"
- )
+ assert (
+ np.linalg.norm(phi) < 1e-6
+ ), f"Constraint violation after projection: {np.linalg.norm(phi)}"
def test_arbitrary_config_projects(self, golfer_params: GolferParams) -> None:
rng = np.random.default_rng(123)
q = rng.uniform(-0.5, 0.5, size=N_DOF)
q_proj = project_to_constraints(q, golfer_params)
phi = constraint_vector(q_proj, golfer_params)
- assert np.linalg.norm(phi) < 1e-4, (
- f"Constraint violation after projection: {np.linalg.norm(phi)}"
- )
+ assert (
+ np.linalg.norm(phi) < 1e-4
+ ), f"Constraint violation after projection: {np.linalg.norm(phi)}"
def test_idempotent(self, golfer_params: GolferParams) -> None:
q = np.zeros(N_DOF)
@@ -107,7 +109,9 @@ def stuck_constraint(_q: np.ndarray, _params: GolferParams) -> np.ndarray:
def constant_jacobian(_q: np.ndarray, _params: GolferParams) -> np.ndarray:
return np.eye(N_CONSTRAINTS, N_DOF)
- monkeypatch.setattr(constraint_solver_module, "constraint_vector", stuck_constraint)
+ monkeypatch.setattr(
+ constraint_solver_module, "constraint_vector", stuck_constraint
+ )
monkeypatch.setattr(
constraint_solver_module,
"constraint_jacobian",
@@ -133,9 +137,9 @@ def test_velocity_satisfies_constraint(self, golfer_params: GolferParams) -> Non
qdot_proj = project_velocity(q, qdot, golfer_params)
Phi_q = constraint_jacobian(q, golfer_params)
violation = Phi_q @ qdot_proj
- assert np.linalg.norm(violation) < 1e-6, (
- f"Velocity constraint violation: {np.linalg.norm(violation)}"
- )
+ assert (
+ np.linalg.norm(violation) < 1e-6
+ ), f"Velocity constraint violation: {np.linalg.norm(violation)}"
class TestConstrainedAccelerations:
@@ -144,7 +148,9 @@ class TestConstrainedAccelerations:
def test_finite_at_rest(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
) -> None:
state = _make_consistent_state(golfer_params)
qddot = constrained_accelerations(state, 0.0, golfer_params, zero_torque)
@@ -154,7 +160,9 @@ def test_finite_at_rest(
def test_shape(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
) -> None:
state = _make_consistent_state(golfer_params)
qddot = constrained_accelerations(state, 0.0, golfer_params, zero_torque)
@@ -167,7 +175,9 @@ class TestConstraintForces:
def test_shape(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
) -> None:
state = _make_consistent_state(golfer_params)
lam = constraint_forces(state, 0.0, golfer_params, zero_torque)
@@ -181,7 +191,9 @@ class TestNativeConstraintBackend:
def test_constrained_dynamics_prefers_native_backend(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
monkeypatch: pytest.MonkeyPatch,
) -> None:
native_qddot = np.full(N_DOF, 3.0)
@@ -255,7 +267,9 @@ class TestEquationsOfMotion:
def test_shape(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
) -> None:
state = _make_consistent_state(golfer_params)
state_dot = equations_of_motion(state, 0.0, golfer_params, zero_torque)
@@ -265,7 +279,9 @@ def test_shape(
def test_velocity_in_derivative(
self,
golfer_params: GolferParams,
- zero_torque: Callable[[float], tuple[float, float, float, float, float, float, float]],
+ zero_torque: Callable[
+ [float], tuple[float, float, float, float, float, float, float]
+ ],
) -> None:
state = _make_consistent_state(golfer_params)
state_dot = equations_of_motion(state, 0.0, golfer_params, zero_torque)
diff --git a/src/pendulum_simulator/tests/test_counterfactual.py b/src/pendulum_simulator/tests/test_counterfactual.py
index fdebb5b74e..d8ea10b859 100644
--- a/src/pendulum_simulator/tests/test_counterfactual.py
+++ b/src/pendulum_simulator/tests/test_counterfactual.py
@@ -47,7 +47,9 @@ def double_state() -> np.ndarray:
@pytest.fixture()
def triple_state() -> np.ndarray:
"""Triple pendulum state: [theta1, phi1, phi2, dtheta1, dphi1, dphi2]."""
- return np.array([np.radians(45.0), np.radians(-30.0), np.radians(20.0), 0.5, -0.3, 0.2])
+ return np.array(
+ [np.radians(45.0), np.radians(-30.0), np.radians(20.0), 0.5, -0.3, 0.2]
+ )
# ---------------------------------------------------------------------------
@@ -83,9 +85,9 @@ def test_zero_velocity_zero_torque_matches_static_gravity(
fx, fy = result["shoulder"]
expected_fy = (double_params.m1 + double_params.m2) * double_params.g
assert abs(fx) < 1e-8, f"No horizontal force at rest, got fx={fx}"
- assert abs(fy - expected_fy) < 1e-4, (
- f"Shoulder fy={fy:.4f}, expected {expected_fy:.4f}"
- )
+ assert (
+ abs(fy - expected_fy) < 1e-4
+ ), f"Shoulder fy={fy:.4f}, expected {expected_fy:.4f}"
def test_differs_from_driven_forces_when_torque_nonzero(
self, double_state: np.ndarray, double_params: PendulumParams
@@ -110,9 +112,9 @@ def torque_func(t: float) -> tuple[float, float]:
# With 50 Nm at shoulder, forces should differ meaningfully
diff_shoulder = abs(actual["shoulder"][1] - counterfactual["shoulder"][1])
- assert diff_shoulder > 1.0, (
- f"Expected driven vs zero-torque to differ; got diff={diff_shoulder:.3f}"
- )
+ assert (
+ diff_shoulder > 1.0
+ ), f"Expected driven vs zero-torque to differ; got diff={diff_shoulder:.3f}"
def test_zero_gravity_hanging_position(self, double_params: PendulumParams) -> None:
"""With g=0, zero-torque counterfactual gives near-zero forces at rest."""
@@ -127,9 +129,9 @@ def test_zero_gravity_hanging_position(self, double_params: PendulumParams) -> N
result = zero_torque_joint_forces_double(state, params_no_g)
for key in ("shoulder", "wrist"):
fx, fy = result[key]
- assert abs(fx) < 1e-8 and abs(fy) < 1e-8, (
- f"No gravity + no motion → zero force at {key}, got ({fx:.2e},{fy:.2e})"
- )
+ assert (
+ abs(fx) < 1e-8 and abs(fy) < 1e-8
+ ), f"No gravity + no motion → zero force at {key}, got ({fx:.2e},{fy:.2e})"
def test_invalid_state_shape_raises(self, double_params: PendulumParams) -> None:
"""Non-(4,) state must raise AssertionError."""
@@ -139,7 +141,9 @@ def test_invalid_state_shape_raises(self, double_params: PendulumParams) -> None
def test_nonfinite_state_raises(self, double_params: PendulumParams) -> None:
"""NaN state must raise AssertionError."""
with pytest.raises((ValueError, TypeError)):
- zero_torque_joint_forces_double(np.array([np.nan, 0.0, 0.0, 0.0]), double_params)
+ zero_torque_joint_forces_double(
+ np.array([np.nan, 0.0, 0.0, 0.0]), double_params
+ )
# ---------------------------------------------------------------------------
@@ -163,11 +167,13 @@ def test_forces_are_finite(
result = zero_torque_joint_forces_triple(triple_state, triple_params)
for key in ("shoulder", "wrist1", "wrist2"):
fx, fy = result[key]
- assert np.isfinite(fx) and np.isfinite(fy), (
- f"{key} forces not finite: ({fx}, {fy})"
- )
+ assert np.isfinite(fx) and np.isfinite(
+ fy
+ ), f"{key} forces not finite: ({fx}, {fy})"
- def test_static_hanging_shoulder_force(self, triple_params: TriplePendulumParams) -> None:
+ def test_static_hanging_shoulder_force(
+ self, triple_params: TriplePendulumParams
+ ) -> None:
"""At rest hanging straight down, shoulder force ≈ (m1+m2+m3)*g."""
state = np.zeros(6)
result = zero_torque_joint_forces_triple(state, triple_params)
@@ -178,6 +184,8 @@ def test_static_hanging_shoulder_force(self, triple_params: TriplePendulumParams
assert abs(fx) < 1e-8
assert abs(fy - expected_fy) < 1e-4, f"fy={fy}, expected {expected_fy}"
- def test_invalid_state_shape_raises(self, triple_params: TriplePendulumParams) -> None:
+ def test_invalid_state_shape_raises(
+ self, triple_params: TriplePendulumParams
+ ) -> None:
with pytest.raises((ValueError, TypeError)):
zero_torque_joint_forces_triple(np.zeros(5), triple_params)
diff --git a/src/pendulum_simulator/tests/test_default_dark_theme_and_button_width.py b/src/pendulum_simulator/tests/test_default_dark_theme_and_button_width.py
index f291a46172..f7945a4bba 100644
--- a/src/pendulum_simulator/tests/test_default_dark_theme_and_button_width.py
+++ b/src/pendulum_simulator/tests/test_default_dark_theme_and_button_width.py
@@ -66,12 +66,12 @@ def test_first_launch_writes_dark_default(self, qapp) -> None:
ensure_default_theme_seeded()
s = QSettings("D-sorganization", "PendulumSimulator")
- assert s.value(_INITIAL_FLAG) is not None, (
- "first_launch_initialized flag should be set after seeding"
- )
- assert s.value(_THEME_KEY) == "Dark", (
- f"Default theme should be 'Dark', got {s.value(_THEME_KEY)!r}"
- )
+ assert (
+ s.value(_INITIAL_FLAG) is not None
+ ), "first_launch_initialized flag should be set after seeding"
+ assert (
+ s.value(_THEME_KEY) == "Dark"
+ ), f"Default theme should be 'Dark', got {s.value(_THEME_KEY)!r}"
def test_existing_user_preference_is_not_overwritten(self, qapp) -> None:
"""If a user already chose 'Light', do not stomp it."""
@@ -86,9 +86,9 @@ def test_existing_user_preference_is_not_overwritten(self, qapp) -> None:
ensure_default_theme_seeded()
- assert s.value(_THEME_KEY) == "Light", (
- "User-chosen 'Light' theme must not be overwritten"
- )
+ assert (
+ s.value(_THEME_KEY) == "Light"
+ ), "User-chosen 'Light' theme must not be overwritten"
def test_seeding_is_idempotent(self, qapp) -> None:
"""Calling ensure_default_theme_seeded twice does not flip
diff --git a/src/pendulum_simulator/tests/test_diagnostics.py b/src/pendulum_simulator/tests/test_diagnostics.py
index 990e480f12..1c954e8d88 100644
--- a/src/pendulum_simulator/tests/test_diagnostics.py
+++ b/src/pendulum_simulator/tests/test_diagnostics.py
@@ -34,7 +34,9 @@ def test_singleton_get_tracker(self) -> Any:
assert t1 is t2
def test_record_event(self, temp_tracker) -> Any:
- temp_tracker.record("test_cat", "test msg", severity="warning", extra={"k": "v"})
+ temp_tracker.record(
+ "test_cat", "test msg", severity="warning", extra={"k": "v"}
+ )
assert len(temp_tracker.events) == 1
event = temp_tracker.events[0]
assert event.category == "test_cat"
@@ -147,7 +149,9 @@ def test_copy_details(self, temp_tracker, qtbot) -> Any:
viewer._table.setCurrentCell(0, 0)
- with patch("double_pendulum_golf.gui.diagnostics.QApplication.clipboard") as mock_clip:
+ with patch(
+ "double_pendulum_golf.gui.diagnostics.QApplication.clipboard"
+ ) as mock_clip:
mock_cb = MagicMock()
mock_clip.return_value = mock_cb
viewer._copy_details()
@@ -179,7 +183,9 @@ def test_hook_records_event(self, temp_tracker) -> Any:
class TestDiagnosticsGaps:
def test_show_viewer(self, temp_tracker) -> Any:
- with patch("double_pendulum_golf.gui.diagnostics.DiagnosticsViewer.exec") as mock_exec:
+ with patch(
+ "double_pendulum_golf.gui.diagnostics.DiagnosticsViewer.exec"
+ ) as mock_exec:
temp_tracker.show_viewer()
mock_exec.assert_called_once()
diff --git a/src/pendulum_simulator/tests/test_dynamics_quantities.py b/src/pendulum_simulator/tests/test_dynamics_quantities.py
index 07a9685d92..93d6efdff5 100644
--- a/src/pendulum_simulator/tests/test_dynamics_quantities.py
+++ b/src/pendulum_simulator/tests/test_dynamics_quantities.py
@@ -58,19 +58,19 @@ class TestLinearPowerAt:
"""Unit tests for single-timestep linear power."""
def test_aligned_force_velocity(self):
- assert linear_power_at(np.array([1.0, 0.0]), np.array([3.0, 0.0])) == pytest.approx(
- 3.0
- )
+ assert linear_power_at(
+ np.array([1.0, 0.0]), np.array([3.0, 0.0])
+ ) == pytest.approx(3.0)
def test_orthogonal_force_velocity(self):
- assert linear_power_at(np.array([1.0, 0.0]), np.array([0.0, 1.0])) == pytest.approx(
- 0.0
- )
+ assert linear_power_at(
+ np.array([1.0, 0.0]), np.array([0.0, 1.0])
+ ) == pytest.approx(0.0)
def test_2d_dot_product(self):
- assert linear_power_at(np.array([2.0, 3.0]), np.array([4.0, 5.0])) == pytest.approx(
- 23.0
- )
+ assert linear_power_at(
+ np.array([2.0, 3.0]), np.array([4.0, 5.0])
+ ) == pytest.approx(23.0)
def test_wrong_shape_raises(self):
with pytest.raises((ValueError, TypeError), match="force must be shape"):
diff --git a/src/pendulum_simulator/tests/test_ellipsoid_scale_and_emoji.py b/src/pendulum_simulator/tests/test_ellipsoid_scale_and_emoji.py
index 4aefacdc45..548a19e279 100644
--- a/src/pendulum_simulator/tests/test_ellipsoid_scale_and_emoji.py
+++ b/src/pendulum_simulator/tests/test_ellipsoid_scale_and_emoji.py
@@ -67,9 +67,9 @@ def test_force_ell_slider_min_emits_one_hundredth(self, qapp) -> None:
ts.force_ell_scale_changed.connect(captured.append)
ts._sld_force_ell.setValue(ts._sld_force_ell.minimum())
assert captured, "force_ell_scale_changed never fired"
- assert captured[-1] <= 0.01 + 1e-9, (
- f"Force ellipsoid slider floor is {captured[-1]}; should be ≤ 0.01"
- )
+ assert (
+ captured[-1] <= 0.01 + 1e-9
+ ), f"Force ellipsoid slider floor is {captured[-1]}; should be ≤ 0.01"
def test_default_value_still_emits_one_x(self, qapp) -> None:
"""The default slider position must still emit 1.0×, so existing
@@ -84,9 +84,9 @@ def test_default_value_still_emits_one_x(self, qapp) -> None:
ts._sld_mob.setValue(default + 1)
ts._sld_mob.setValue(default)
assert captured, "mob_scale_changed did not fire on default"
- assert captured[-1] == pytest.approx(1.0, abs=0.05), (
- f"Default mob scale should be ~1.0×, got {captured[-1]}"
- )
+ assert captured[-1] == pytest.approx(
+ 1.0, abs=0.05
+ ), f"Default mob scale should be ~1.0×, got {captured[-1]}"
# ──────────────────────────────────────────────────────────────────────
diff --git a/src/pendulum_simulator/tests/test_friction.py b/src/pendulum_simulator/tests/test_friction.py
index f5ad7a6015..f7087e0087 100644
--- a/src/pendulum_simulator/tests/test_friction.py
+++ b/src/pendulum_simulator/tests/test_friction.py
@@ -125,25 +125,33 @@ def test_viscous_magnitude_linear(self, damped_params: PendulumParams) -> None:
expected_tau_f1 = -damped_params.b1 * dtheta1
assert np.isclose(tf[0], expected_tau_f1)
- def test_coulomb_has_constant_magnitude(self, frictional_params: PendulumParams) -> None:
+ def test_coulomb_has_constant_magnitude(
+ self, frictional_params: PendulumParams
+ ) -> None:
"""Coulomb friction magnitude is mu regardless of velocity magnitude."""
for speed in [0.1, 1.0, 10.0, 100.0]:
- tf = friction_torque_vector(dtheta1=speed, dphi=speed, params=frictional_params)
- assert np.isclose(abs(tf[0]), frictional_params.mu1), (
- f"Expected |tau_f1|={frictional_params.mu1}, got {abs(tf[0])} at speed={speed}"
+ tf = friction_torque_vector(
+ dtheta1=speed, dphi=speed, params=frictional_params
)
+ assert np.isclose(
+ abs(tf[0]), frictional_params.mu1
+ ), f"Expected |tau_f1|={frictional_params.mu1}, got {abs(tf[0])} at speed={speed}"
def test_coulomb_zero_at_rest(self, frictional_params: PendulumParams) -> None:
"""np.sign(0) == 0, so Coulomb friction is zero when stationary."""
tf = friction_torque_vector(dtheta1=0.0, dphi=0.0, params=frictional_params)
assert np.allclose(tf, [0.0, 0.0])
- def test_combined_friction_superposition(self, combined_params: PendulumParams) -> None:
+ def test_combined_friction_superposition(
+ self, combined_params: PendulumParams
+ ) -> None:
"""Combined damping+friction = viscous + Coulomb separately."""
dtheta1, dphi = 1.5, -0.8
tf = friction_torque_vector(dtheta1, dphi, combined_params)
- expected_1 = -combined_params.b1 * dtheta1 - combined_params.mu1 * np.sign(dtheta1)
+ expected_1 = -combined_params.b1 * dtheta1 - combined_params.mu1 * np.sign(
+ dtheta1
+ )
expected_2 = -combined_params.b2 * dphi - combined_params.mu2 * np.sign(dphi)
assert np.isclose(tf[0], expected_1)
assert np.isclose(tf[1], expected_2)
@@ -182,9 +190,9 @@ def test_undamped_conserves_energy_approximately(
e_start = total_energy(result.states[0], base_params)
e_end = total_energy(result.states[-1], base_params)
# Allow ~1% drift from numerical integration
- assert abs(e_end - e_start) / max(abs(e_start), 1e-9) < 0.01, (
- f"Energy drift too large: {e_start:.4f} → {e_end:.4f}"
- )
+ assert (
+ abs(e_end - e_start) / max(abs(e_start), 1e-9) < 0.01
+ ), f"Energy drift too large: {e_start:.4f} → {e_end:.4f}"
def test_damped_pendulum_loses_energy(self, damped_params: PendulumParams) -> None:
"""With viscous damping, total energy must decrease over time."""
@@ -203,9 +211,9 @@ def test_damped_pendulum_loses_energy(self, damped_params: PendulumParams) -> No
e_start = total_energy(result.states[0], damped_params)
e_end = total_energy(result.states[-1], damped_params)
- assert e_end < e_start, (
- f"Damped pendulum energy should decrease: {e_start:.4f} → {e_end:.4f}"
- )
+ assert (
+ e_end < e_start
+ ), f"Damped pendulum energy should decrease: {e_start:.4f} → {e_end:.4f}"
def test_friction_does_not_blow_up(self, combined_params: PendulumParams) -> None:
"""Simulation with both friction types must remain numerically stable."""
@@ -221,9 +229,9 @@ def test_friction_does_not_blow_up(self, combined_params: PendulumParams) -> Non
)
assert result.n_steps >= 2
- assert all(np.isfinite(result.states.flatten())), (
- "Simulation with combined friction/damping produced non-finite states"
- )
+ assert all(
+ np.isfinite(result.states.flatten())
+ ), "Simulation with combined friction/damping produced non-finite states"
# ---------------------------------------------------------------------------
@@ -259,7 +267,9 @@ def test_total_torques_equals_drive_plus_friction(
total = friction_result.total_torques_at(idx)
assert np.allclose(total, drive + friction)
- def test_no_dissipation_zero_friction_torques(self, base_params: PendulumParams) -> None:
+ def test_no_dissipation_zero_friction_torques(
+ self, base_params: PendulumParams
+ ) -> None:
state0 = np.array([np.radians(45), 0.0, 1.0, 0.0])
result = run_simulation(
params=base_params,
@@ -270,6 +280,6 @@ def test_no_dissipation_zero_friction_torques(self, base_params: PendulumParams)
)
for i in range(0, result.n_steps, 20):
tf = result.friction_torques_at(i)
- assert np.allclose(tf, [0.0, 0.0]), (
- f"Expected zero friction torques at step {i}, got {tf}"
- )
+ assert np.allclose(
+ tf, [0.0, 0.0]
+ ), f"Expected zero friction torques at step {i}, got {tf}"
diff --git a/src/pendulum_simulator/tests/test_friction_triple.py b/src/pendulum_simulator/tests/test_friction_triple.py
index 1c25429db4..ae300a72ac 100644
--- a/src/pendulum_simulator/tests/test_friction_triple.py
+++ b/src/pendulum_simulator/tests/test_friction_triple.py
@@ -295,7 +295,9 @@ def test_combined_friction_superposition(
dtheta1, dphi1, dphi2 = 1.5, -0.8, 0.3
tf = friction_torque_vector(dtheta1, dphi1, dphi2, combined_params)
- expected_1 = -combined_params.b1 * dtheta1 - combined_params.mu1 * np.sign(dtheta1)
+ expected_1 = -combined_params.b1 * dtheta1 - combined_params.mu1 * np.sign(
+ dtheta1
+ )
expected_2 = -combined_params.b2 * dphi1 - combined_params.mu2 * np.sign(dphi1)
expected_3 = -combined_params.b3 * dphi2 - combined_params.mu3 * np.sign(dphi2)
assert np.isclose(tf[0], expected_1)
@@ -345,9 +347,9 @@ def test_undamped_conserves_energy_approximately(
e_start = total_energy(result.states[0], base_params)
e_end = total_energy(result.states[-1], base_params)
# Allow ~2% drift for chaotic triple pendulum
- assert abs(e_end - e_start) / max(abs(e_start), 1e-9) < 0.02, (
- f"Energy drift too large: {e_start:.4f} → {e_end:.4f}"
- )
+ assert (
+ abs(e_end - e_start) / max(abs(e_start), 1e-9) < 0.02
+ ), f"Energy drift too large: {e_start:.4f} → {e_end:.4f}"
def test_damped_pendulum_loses_energy(
self,
@@ -369,16 +371,18 @@ def test_damped_pendulum_loses_energy(
e_start = total_energy(result.states[0], damped_params)
e_end = total_energy(result.states[-1], damped_params)
- assert e_end < e_start, (
- f"Damped pendulum energy should decrease: {e_start:.4f} → {e_end:.4f}"
- )
+ assert (
+ e_end < e_start
+ ), f"Damped pendulum energy should decrease: {e_start:.4f} → {e_end:.4f}"
def test_friction_does_not_blow_up(
self,
combined_params: TriplePendulumParams,
) -> None:
"""Simulation with both friction types must remain numerically stable."""
- state0 = np.array([np.radians(90), np.radians(-45), np.radians(30), 0.0, 0.0, 0.0])
+ state0 = np.array(
+ [np.radians(90), np.radians(-45), np.radians(30), 0.0, 0.0, 0.0]
+ )
torque_func = make_polynomial_torque([-15.0, 5.0], [0.0], [0.0])
result = run_simulation(
@@ -390,9 +394,9 @@ def test_friction_does_not_blow_up(
)
assert result.n_steps >= 2
- assert all(np.isfinite(result.states.flatten())), (
- "Simulation with combined friction/damping produced non-finite states"
- )
+ assert all(
+ np.isfinite(result.states.flatten())
+ ), "Simulation with combined friction/damping produced non-finite states"
# ---------------------------------------------------------------------------
@@ -472,7 +476,9 @@ class TestMassMatrixCorrectness:
def equal_params(self) -> TriplePendulumParams:
return TriplePendulumParams(m1=1.0, m2=1.0, m3=1.0, L1=1.0, L2=1.0, L3=1.0)
- def test_symmetric_at_random_angles(self, equal_params: TriplePendulumParams) -> None:
+ def test_symmetric_at_random_angles(
+ self, equal_params: TriplePendulumParams
+ ) -> None:
rng = np.random.default_rng(42)
for _ in range(20):
phi1, phi2 = rng.uniform(-np.pi, np.pi, size=2)
@@ -487,7 +493,9 @@ def test_positive_definite_at_random_angles(
phi1, phi2 = rng.uniform(-np.pi, np.pi, size=2)
M = mass_matrix(phi1, phi2, equal_params)
eigvals = np.linalg.eigvalsh(M)
- assert all(eigvals > 0), f"Not positive definite at phi1={phi1}, phi2={phi2}"
+ assert all(
+ eigvals > 0
+ ), f"Not positive definite at phi1={phi1}, phi2={phi2}"
def test_aligned_configuration_known_value(self) -> None:
"""When phi1=phi2=0 (all segments aligned), M has a known closed form."""
@@ -550,9 +558,11 @@ def test_conservative_energy_conservation(self, state0: np.ndarray) -> None:
rtol=1e-10,
atol=1e-12,
)
- energies = [total_energy(result.states[i], params) for i in range(result.n_steps)]
+ energies = [
+ total_energy(result.states[i], params) for i in range(result.n_steps)
+ ]
e0 = energies[0]
max_drift = max(abs(e - e0) for e in energies)
- assert max_drift < 1e-6, (
- f"Energy drift {max_drift:.2e} exceeds 1e-6 for state0={state0}"
- )
+ assert (
+ max_drift < 1e-6
+ ), f"Energy drift {max_drift:.2e} exceeds 1e-6 for state0={state0}"
diff --git a/src/pendulum_simulator/tests/test_golfer_dynamics_extended.py b/src/pendulum_simulator/tests/test_golfer_dynamics_extended.py
index 263abcab6b..fe1598a17e 100644
--- a/src/pendulum_simulator/tests/test_golfer_dynamics_extended.py
+++ b/src/pendulum_simulator/tests/test_golfer_dynamics_extended.py
@@ -293,7 +293,9 @@ def test_symmetric(self, params: GolferParams) -> None:
M = analytical_mass_matrix(q, params)
np.testing.assert_allclose(M, M.T, atol=1e-8)
- def test_positive_semidefinite(self, params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_positive_semidefinite(
+ self, params: GolferParams, zero_q: np.ndarray
+ ) -> None:
with patch(
"double_pendulum_golf.golfer_dynamics._native_backend.golfer_mass_matrix",
return_value=None,
@@ -440,7 +442,9 @@ def test_zero_at_rest(
T = kinetic_energy(zero_q, zero_qdot, params)
assert T == pytest.approx(0.0, abs=1e-12)
- def test_positive_with_velocity(self, params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_positive_with_velocity(
+ self, params: GolferParams, zero_q: np.ndarray
+ ) -> None:
qdot = np.ones(N_DOF) * 0.5
with patch(
"double_pendulum_golf.golfer_dynamics._native_backend.golfer_mass_matrix",
@@ -462,7 +466,9 @@ def test_scales_quadratically_with_speed(
T2 = kinetic_energy(zero_q, 2 * qdot, params)
assert T2 == pytest.approx(4 * T1, rel=1e-6)
- def test_type_error_non_array_q(self, params: GolferParams, zero_qdot: np.ndarray) -> None:
+ def test_type_error_non_array_q(
+ self, params: GolferParams, zero_qdot: np.ndarray
+ ) -> None:
with pytest.raises(TypeError):
kinetic_energy([0.0] * N_DOF, zero_qdot, params)
@@ -490,7 +496,9 @@ def test_returns_float(self, params: GolferParams, full_state: np.ndarray) -> No
V = potential_energy(full_state, params)
assert isinstance(V, float)
- def test_different_configurations_give_different_pe(self, params: GolferParams) -> None:
+ def test_different_configurations_give_different_pe(
+ self, params: GolferParams
+ ) -> None:
q1 = np.zeros(N_DOF)
state1 = np.concatenate([q1, np.zeros(N_DOF)])
q2 = np.zeros(N_DOF)
@@ -554,7 +562,9 @@ def test_total_is_T_plus_V(self, params: GolferParams) -> None:
class TestMassPointPositions:
- def test_returns_seven_points(self, params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_returns_seven_points(
+ self, params: GolferParams, zero_q: np.ndarray
+ ) -> None:
points = _mass_point_positions(zero_q, params)
assert len(points) == 7
@@ -564,14 +574,18 @@ def test_all_callable(self, params: GolferParams, zero_q: np.ndarray) -> None:
result = pos_func(zero_q)
assert len(result) == 2
- def test_masses_match_params(self, params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_masses_match_params(
+ self, params: GolferParams, zero_q: np.ndarray
+ ) -> None:
points = _mass_point_positions(zero_q, params)
masses = [m for m, _ in points]
assert params.m_hub in masses
assert params.m_r_upper in masses
assert params.m_club in masses
- def test_all_positions_finite(self, params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_all_positions_finite(
+ self, params: GolferParams, zero_q: np.ndarray
+ ) -> None:
points = _mass_point_positions(zero_q, params)
for _, pos_func in points:
x, y = pos_func(zero_q)
diff --git a/src/pendulum_simulator/tests/test_golfer_ellipsoids.py b/src/pendulum_simulator/tests/test_golfer_ellipsoids.py
index 4d3ec77f67..514803bd00 100644
--- a/src/pendulum_simulator/tests/test_golfer_ellipsoids.py
+++ b/src/pendulum_simulator/tests/test_golfer_ellipsoids.py
@@ -86,9 +86,9 @@ def test_nonzero_configuration(self, default_golfer_params):
result = ellipsoids_golfer(q, default_golfer_params)
assert len(result) > 0
for name, ell in result.items():
- assert np.all(np.isfinite(ell["mob_semi_axes"])), (
- f"{name}: non-finite mob_semi_axes"
- )
+ assert np.all(
+ np.isfinite(ell["mob_semi_axes"])
+ ), f"{name}: non-finite mob_semi_axes"
def test_handles_full_state_vector(self, default_golfer_params):
"""Should accept q with shape (16,) and use only first 8."""
diff --git a/src/pendulum_simulator/tests/test_golfer_kinematics.py b/src/pendulum_simulator/tests/test_golfer_kinematics.py
index bc7bd40382..c9e1a7dfa6 100644
--- a/src/pendulum_simulator/tests/test_golfer_kinematics.py
+++ b/src/pendulum_simulator/tests/test_golfer_kinematics.py
@@ -120,7 +120,9 @@ def test_pi_half_hub_to_the_left(self, sym_params: GolferParams) -> None:
assert x == pytest.approx(-sym_params.L_hub, abs=1e-10)
assert abs(y) < 1e-10
- def test_hub_distance_from_origin_equals_L_hub(self, sym_params: GolferParams) -> None:
+ def test_hub_distance_from_origin_equals_L_hub(
+ self, sym_params: GolferParams
+ ) -> None:
"""Distance |hub| must equal L_hub for all angles."""
for theta in np.linspace(-np.pi, np.pi, 20):
x, y = _hub_position(theta, sym_params)
@@ -141,7 +143,9 @@ def test_returns_tuple_of_two_floats(self, sym_params: GolferParams) -> None:
class TestShoulderPosition:
- def test_distance_from_hub_equals_d_shoulder(self, sym_params: GolferParams) -> None:
+ def test_distance_from_hub_equals_d_shoulder(
+ self, sym_params: GolferParams
+ ) -> None:
hub = (0.0, sym_params.L_hub)
for d in [0.15, 0.20, 0.25]:
rs = _shoulder_position(hub, 0.0, d, +1.0)
@@ -274,9 +278,9 @@ def test_all_positions_finite(self, sym_params: GolferParams) -> None:
q = rng.uniform(-np.pi / 2, np.pi / 2, N_DOF)
pos = self._fk(q, sym_params)
for key, (x, y) in pos.items():
- assert np.isfinite(x) and np.isfinite(y), (
- f"Non-finite position for joint {key!r}: ({x}, {y})"
- )
+ assert np.isfinite(x) and np.isfinite(
+ y
+ ), f"Non-finite position for joint {key!r}: ({x}, {y})"
def test_origin_always_zero(self, sym_params: GolferParams) -> None:
for q in [np.zeros(N_DOF), np.ones(N_DOF) * 0.3]:
@@ -355,7 +359,9 @@ def test_extended_state_is_truncated(self, sym_params: GolferParams) -> None:
assert pos_ext[key][0] == pytest.approx(pos_short[key][0], abs=1e-10)
assert pos_ext[key][1] == pytest.approx(pos_short[key][1], abs=1e-10)
- def test_scapula_keys_present_when_nonzero(self, scapula_params: GolferParams) -> None:
+ def test_scapula_keys_present_when_nonzero(
+ self, scapula_params: GolferParams
+ ) -> None:
"""When L_rscap > 0, 'rscap' and 'lscap' should appear in the result."""
q = np.zeros(N_DOF)
pos = self._fk(q, scapula_params)
diff --git a/src/pendulum_simulator/tests/test_golfer_model.py b/src/pendulum_simulator/tests/test_golfer_model.py
index 604cef1692..adfad92267 100644
--- a/src/pendulum_simulator/tests/test_golfer_model.py
+++ b/src/pendulum_simulator/tests/test_golfer_model.py
@@ -197,9 +197,9 @@ def test_friction_opposes_velocity(self, default_params: GolferParams) -> None:
# For each DOF with nonzero damping, sign(tau) = -sign(qdot)
for i in range(N_DOF - 1): # Skip club DOF (no damping)
if abs(qdot[i]) > 0 and abs(tau[i]) > 0:
- assert np.sign(tau[i]) == -np.sign(qdot[i]), (
- f"Friction at DOF {i} does not oppose velocity"
- )
+ assert np.sign(tau[i]) == -np.sign(
+ qdot[i]
+ ), f"Friction at DOF {i} does not oppose velocity"
def test_zero_velocity_zero_friction(self, default_params: GolferParams) -> None:
"""Zero velocity must produce zero friction torque."""
@@ -257,9 +257,9 @@ def test_mass_matrix_psd(
M = analytical_mass_matrix(random_state, default_params)
eigenvalues = np.linalg.eigvalsh(M)
- assert np.all(eigenvalues >= -1e-10), (
- f"Negative eigenvalue in mass matrix: {eigenvalues}"
- )
+ assert np.all(
+ eigenvalues >= -1e-10
+ ), f"Negative eigenvalue in mass matrix: {eigenvalues}"
def test_mass_matrix_shape(
self, default_params: GolferParams, zero_state: np.ndarray
diff --git a/src/pendulum_simulator/tests/test_golfer_moments.py b/src/pendulum_simulator/tests/test_golfer_moments.py
index 49ceb17b72..8b28b93221 100644
--- a/src/pendulum_simulator/tests/test_golfer_moments.py
+++ b/src/pendulum_simulator/tests/test_golfer_moments.py
@@ -74,9 +74,9 @@ def test_total_equals_applied_plus_moment(self, sample_positions, sample_forces)
applied = result[f"{jname}_applied_torque"]
moment = result[f"{jname}_moment_of_force"]
total = result[f"{jname}_total_moment"]
- assert total == pytest.approx(applied + moment), (
- f"{jname}: total {total} != applied {applied} + moment {moment}"
- )
+ assert total == pytest.approx(
+ applied + moment
+ ), f"{jname}: total {total} != applied {applied} + moment {moment}"
def test_too_few_torques_raises(self, sample_positions, sample_forces):
"""Must have at least 7 applied torques."""
diff --git a/src/pendulum_simulator/tests/test_golfer_topology.py b/src/pendulum_simulator/tests/test_golfer_topology.py
index 69d854e889..f2c25e1ff4 100644
--- a/src/pendulum_simulator/tests/test_golfer_topology.py
+++ b/src/pendulum_simulator/tests/test_golfer_topology.py
@@ -62,9 +62,9 @@ class TestStandoffMassless:
def test_standoff_mass_near_zero(self, address_params: GolferParams) -> None:
"""Standoff mass must be near zero (< 0.01 kg)."""
- assert address_params.m_hub < 0.01, (
- f"Standoff mass should be near-zero, got {address_params.m_hub}"
- )
+ assert (
+ address_params.m_hub < 0.01
+ ), f"Standoff mass should be near-zero, got {address_params.m_hub}"
def test_standoff_mass_positive(self, address_params: GolferParams) -> None:
"""Standoff mass must be positive (required by solver numerics)."""
@@ -78,7 +78,9 @@ def test_standoff_has_length(self, address_params: GolferParams) -> None:
class TestUpperBodyMass:
"""Upper body (scapula) segments should have significant mass (~2x arms)."""
- def test_right_upper_body_heavier_than_arms(self, address_params: GolferParams) -> None:
+ def test_right_upper_body_heavier_than_arms(
+ self, address_params: GolferParams
+ ) -> None:
"""Right upper body mass should be >= right arm total."""
right_arm_total = address_params.m_r_upper + address_params.m_r_fore
assert address_params.m_rscap >= right_arm_total, (
@@ -86,7 +88,9 @@ def test_right_upper_body_heavier_than_arms(self, address_params: GolferParams)
f"right arm total ({right_arm_total} kg)"
)
- def test_left_upper_body_heavier_than_arms(self, address_params: GolferParams) -> None:
+ def test_left_upper_body_heavier_than_arms(
+ self, address_params: GolferParams
+ ) -> None:
"""Left upper body mass should be >= left arm total."""
left_arm_total = address_params.m_l_upper + address_params.m_l_fore
assert address_params.m_lscap >= left_arm_total, (
@@ -124,7 +128,9 @@ def test_total_mass_reasonable(self, address_params: GolferParams) -> None:
+ address_params.m_clubhead
)
# Upper body + arms + club: roughly 10-40 kg is reasonable
- assert 10.0 < total < 40.0, f"Total mass {total:.1f} kg should be in 10-40 kg range"
+ assert (
+ 10.0 < total < 40.0
+ ), f"Total mass {total:.1f} kg should be in 10-40 kg range"
def test_standoff_negligible_fraction(self, address_params: GolferParams) -> None:
"""Standoff mass should be < 0.1% of total system mass."""
@@ -140,7 +146,9 @@ def test_standoff_negligible_fraction(self, address_params: GolferParams) -> Non
+ address_params.m_clubhead
)
fraction = address_params.m_hub / total
- assert fraction < 0.001, f"Standoff mass fraction {fraction:.4f} should be < 0.001"
+ assert (
+ fraction < 0.001
+ ), f"Standoff mass fraction {fraction:.4f} should be < 0.001"
def test_upper_body_dominates(self, address_params: GolferParams) -> None:
"""Upper body segments should be the heaviest components."""
@@ -153,12 +161,12 @@ def test_upper_body_dominates(self, address_params: GolferParams) -> None:
address_params.m_club,
address_params.m_clubhead,
]
- assert address_params.m_rscap >= max(all_masses), (
- "Right upper body should be the heaviest individual segment"
- )
- assert address_params.m_lscap >= max(all_masses), (
- "Left upper body should be the heaviest individual segment"
- )
+ assert address_params.m_rscap >= max(
+ all_masses
+ ), "Right upper body should be the heaviest individual segment"
+ assert address_params.m_lscap >= max(
+ all_masses
+ ), "Left upper body should be the heaviest individual segment"
class TestGolferParamsValidation:
@@ -256,7 +264,9 @@ def test_positions_finite(self, address_params: GolferParams) -> None:
q = np.zeros(8)
pos = forward_kinematics(q, address_params)
for name, xy in pos.items():
- assert np.all(np.isfinite(xy)), f"Position {name} has non-finite values: {xy}"
+ assert np.all(
+ np.isfinite(xy)
+ ), f"Position {name} has non-finite values: {xy}"
def test_scapula_positions_present(self, address_params: GolferParams) -> None:
"""When scapula lengths are nonzero, scapula positions must be in FK."""
diff --git a/src/pendulum_simulator/tests/test_gui_utilities.py b/src/pendulum_simulator/tests/test_gui_utilities.py
index ec3366b33a..30e146b23f 100644
--- a/src/pendulum_simulator/tests/test_gui_utilities.py
+++ b/src/pendulum_simulator/tests/test_gui_utilities.py
@@ -317,7 +317,9 @@ def test_all_factors_positive(self) -> None:
for cat, options in _UNIT_OPTIONS.items():
for label, factor in options:
- assert factor > 0, f"Non-positive factor for {cat.value}/{label}: {factor}"
+ assert (
+ factor > 0
+ ), f"Non-positive factor for {cat.value}/{label}: {factor}"
class TestToSiFromSi:
diff --git a/src/pendulum_simulator/tests/test_hub_and_geometry.py b/src/pendulum_simulator/tests/test_hub_and_geometry.py
index d500a211e9..7a9e347ba4 100644
--- a/src/pendulum_simulator/tests/test_hub_and_geometry.py
+++ b/src/pendulum_simulator/tests/test_hub_and_geometry.py
@@ -176,7 +176,9 @@ def test_finite(self) -> None:
def test_negative_radius_raises(self) -> None:
with pytest.raises((ValueError, TypeError)):
- cylinder_cross_section(np.array([0.0, 0.0]), np.array([1.0, 0.0]), radius=-0.1)
+ cylinder_cross_section(
+ np.array([0.0, 0.0]), np.array([1.0, 0.0]), radius=-0.1
+ )
def test_degenerate_segment(self) -> None:
"""Zero-length segment should not crash."""
@@ -233,7 +235,9 @@ class TestTaperedCylinderCrossSection:
def test_shape(self) -> None:
start = np.array([0.0, 0.0])
end = np.array([0.0, 1.0])
- corners = tapered_cylinder_cross_section(start, end, radius_start=0.2, radius_end=0.05)
+ corners = tapered_cylinder_cross_section(
+ start, end, radius_start=0.2, radius_end=0.05
+ )
assert corners.shape == (4, 2)
def test_finite(self) -> None:
diff --git a/src/pendulum_simulator/tests/test_hypothesis_physics.py b/src/pendulum_simulator/tests/test_hypothesis_physics.py
index eb8b6153e5..ae1a7741db 100644
--- a/src/pendulum_simulator/tests/test_hypothesis_physics.py
+++ b/src/pendulum_simulator/tests/test_hypothesis_physics.py
@@ -137,13 +137,17 @@ def test_kinetic_energy_non_negative(
@given(params=double_params(), state=double_state())
@settings(max_examples=50)
- def test_total_energy_finite(self, params: PendulumParams, state: np.ndarray) -> None:
+ def test_total_energy_finite(
+ self, params: PendulumParams, state: np.ndarray
+ ) -> None:
E = total_energy(state, params)
assert np.isfinite(E), f"Non-finite total energy: {E}"
@given(params=double_params(), state=double_state())
@settings(max_examples=50)
- def test_total_energy_is_sum(self, params: PendulumParams, state: np.ndarray) -> None:
+ def test_total_energy_is_sum(
+ self, params: PendulumParams, state: np.ndarray
+ ) -> None:
T = kinetic_energy(state, params)
V = potential_energy(state, params)
E = total_energy(state, params)
@@ -161,15 +165,15 @@ def test_fk_segment_lengths(
# Shoulder at origin, wrist distance = L1
wrist_dist = np.linalg.norm(wrist)
- assert np.isclose(wrist_dist, params.L1, atol=1e-8), (
- f"Wrist distance {wrist_dist} != L1 {params.L1}"
- )
+ assert np.isclose(
+ wrist_dist, params.L1, atol=1e-8
+ ), f"Wrist distance {wrist_dist} != L1 {params.L1}"
# Wrist-to-tip distance = L2
tip_dist = np.linalg.norm(tip - wrist)
- assert np.isclose(tip_dist, params.L2, atol=1e-8), (
- f"Tip distance {tip_dist} != L2 {params.L2}"
- )
+ assert np.isclose(
+ tip_dist, params.L2, atol=1e-8
+ ), f"Tip distance {tip_dist} != L2 {params.L2}"
# ---------------------------------------------------------------------------
@@ -218,7 +222,9 @@ def test_total_energy_is_sum(
@given(params=triple_params(), state=triple_state())
@settings(max_examples=30)
- def test_fk_segment_lengths(self, params: TriplePendulumParams, state: np.ndarray) -> None:
+ def test_fk_segment_lengths(
+ self, params: TriplePendulumParams, state: np.ndarray
+ ) -> None:
"""FK inter-joint distances must match segment lengths."""
pos = triple_fk(state[0], state[1], state[2], params)
shoulder = np.array(pos["shoulder"])
diff --git a/src/pendulum_simulator/tests/test_issue_fixes.py b/src/pendulum_simulator/tests/test_issue_fixes.py
index 0f1ce9fa7e..0523ddad58 100644
--- a/src/pendulum_simulator/tests/test_issue_fixes.py
+++ b/src/pendulum_simulator/tests/test_issue_fixes.py
@@ -164,7 +164,9 @@ def test_plot_data_stores(self) -> None:
# ---------------------------------------------------------------------------
-@pytest.mark.skipif(not _has_pyqt6(), reason="PyQt6 not available in headless environment")
+@pytest.mark.skipif(
+ not _has_pyqt6(), reason="PyQt6 not available in headless environment"
+)
class TestBasePendulumWidget3D:
"""3D segment rendering base class methods must exist and be callable."""
@@ -227,7 +229,9 @@ def test_tilt_foreshortens_y(self) -> None:
# ---------------------------------------------------------------------------
-@pytest.mark.skipif(not _has_pyqt6(), reason="PyQt6 not available in headless environment")
+@pytest.mark.skipif(
+ not _has_pyqt6(), reason="PyQt6 not available in headless environment"
+)
class TestFunctionGeneratorDialog:
"""Function generator dialog must be importable with correct structure."""
@@ -280,9 +284,9 @@ def test_no_print_in_optimizer_gpu(self) -> None:
stripped = line.strip()
if stripped.startswith("#") or stripped.startswith('"'):
continue
- assert "print(" not in stripped, (
- f"optimizer_gpu.py line {i}: found print() call"
- )
+ assert (
+ "print(" not in stripped
+ ), f"optimizer_gpu.py line {i}: found print() call"
except ImportError:
pytest.skip("optimizer_gpu not available")
@@ -468,7 +472,9 @@ def test_simulation_rejects_nonfinite_state(self) -> None:
def test_noise_generator_rejects_negative_amplitude(self) -> None:
from double_pendulum_golf.perturbation_analysis import generate_noise
- with pytest.raises((ValueError, TypeError), match="amplitude must be non-negative"):
+ with pytest.raises(
+ (ValueError, TypeError), match="amplitude must be non-negative"
+ ):
generate_noise("white", 100, -1.0)
def test_noise_generator_rejects_zero_samples(self) -> None:
diff --git a/src/pendulum_simulator/tests/test_jacobians.py b/src/pendulum_simulator/tests/test_jacobians.py
index 44637867bf..f93a4b0033 100644
--- a/src/pendulum_simulator/tests/test_jacobians.py
+++ b/src/pendulum_simulator/tests/test_jacobians.py
@@ -136,9 +136,9 @@ def test_phi_only_affects_tip_not_wrist(self, L: tuple[float, float]) -> None:
L1, L2 = L
for phi in [0.0, 0.3, 1.0, -0.8]:
J_wrist = jacobian_double(0.5, phi, L1, L2)["wrist"]
- assert np.isclose(J_wrist[0, 1], 0.0), (
- f"J_wrist[:,1] should be zero for any phi, got {J_wrist[:, 1]}"
- )
+ assert np.isclose(
+ J_wrist[0, 1], 0.0
+ ), f"J_wrist[:,1] should be zero for any phi, got {J_wrist[:, 1]}"
class TestJacobianDoubleContinuity:
@@ -150,9 +150,9 @@ def test_continuity_at_various_angles(self, L: tuple[float, float]) -> None:
for theta1 in np.linspace(-1.0, 1.0, 10):
J0 = jacobian_double(theta1, 0.5, L1, L2)["tip"]
J1 = jacobian_double(theta1 + eps, 0.5, L1, L2)["tip"]
- assert np.allclose(J0, J1, atol=(L1 + L2) * eps * 2), (
- f"Jacobian discontinuity at theta1={theta1}"
- )
+ assert np.allclose(
+ J0, J1, atol=(L1 + L2) * eps * 2
+ ), f"Jacobian discontinuity at theta1={theta1}"
# ============================================================================
@@ -174,7 +174,9 @@ def test_all_jacobians_shape(self, L3: tuple[float, float, float]) -> None:
class TestJacobianTripleAnalytic:
"""Known values at canonical configurations."""
- def test_straight_down_wrist1_jacobian(self, L3: tuple[float, float, float]) -> None:
+ def test_straight_down_wrist1_jacobian(
+ self, L3: tuple[float, float, float]
+ ) -> None:
"""theta1=phi1=phi2=0 → wrist1: [[L1, 0, 0], [0, 0, 0]]."""
L1, L2, L3_ = L3
J = jacobian_triple(0.0, 0.0, 0.0, L1, L2, L3_)["wrist1"]
@@ -329,15 +331,17 @@ def test_each_endpoint_has_required_keys(self, L: tuple[float, float]) -> None:
"singular_values",
}
for name, data in result.items():
- assert set(data.keys()) == expected_keys, (
- f"Missing keys in '{name}': {expected_keys - set(data.keys())}"
- )
+ assert (
+ set(data.keys()) == expected_keys
+ ), f"Missing keys in '{name}': {expected_keys - set(data.keys())}"
def test_mob_axes_positive_full_rank(self, L: tuple[float, float]) -> None:
L1, L2 = L
result = ellipsoids_double(1.0, 0.5, L1, L2)
for name, data in result.items():
- assert np.all(data["mob_semi_axes"] >= 0), f"Negative mobility axis in '{name}'"
+ assert np.all(
+ data["mob_semi_axes"] >= 0
+ ), f"Negative mobility axis in '{name}'"
class TestEllipsoidsTriple:
@@ -348,7 +352,9 @@ def test_returns_three_endpoints(self, L3: tuple[float, float, float]) -> None:
result = ellipsoids_triple(0.3, 0.2, 0.1, L1, L2, L3_)
assert set(result.keys()) == {"wrist1", "wrist2", "tip"}
- def test_each_endpoint_has_required_keys(self, L3: tuple[float, float, float]) -> None:
+ def test_each_endpoint_has_required_keys(
+ self, L3: tuple[float, float, float]
+ ) -> None:
L1, L2, L3_ = L3
result = ellipsoids_triple(0.3, 0.2, 0.1, L1, L2, L3_)
required = {
diff --git a/src/pendulum_simulator/tests/test_jacobians_extended.py b/src/pendulum_simulator/tests/test_jacobians_extended.py
index 37a675285d..cbc5419ecf 100644
--- a/src/pendulum_simulator/tests/test_jacobians_extended.py
+++ b/src/pendulum_simulator/tests/test_jacobians_extended.py
@@ -224,11 +224,15 @@ def test_singular_values_non_negative(self) -> None:
class TestJacobianGolfer:
- def test_returns_dict(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_returns_dict(
+ self, golfer_params: GolferParams, zero_q: np.ndarray
+ ) -> None:
J = jacobian_golfer(zero_q, golfer_params)
assert isinstance(J, dict)
- def test_joint_key_shapes(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_joint_key_shapes(
+ self, golfer_params: GolferParams, zero_q: np.ndarray
+ ) -> None:
J = jacobian_golfer(zero_q, golfer_params)
for name, mat in J.items():
assert mat.shape == (2, N_DOF), f"Wrong shape for joint {name}"
@@ -240,7 +244,9 @@ def test_finite(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
class TestEllipsoidsGolfer:
- def test_returns_dict(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_returns_dict(
+ self, golfer_params: GolferParams, zero_q: np.ndarray
+ ) -> None:
result = ellipsoids_golfer(zero_q, golfer_params)
assert isinstance(result, dict)
@@ -272,7 +278,9 @@ def test_finite(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
Z = ztcf_matrix(zero_q, golfer_params)
assert np.all(np.isfinite(Z))
- def test_different_joint(self, golfer_params: GolferParams, zero_q: np.ndarray) -> None:
+ def test_different_joint(
+ self, golfer_params: GolferParams, zero_q: np.ndarray
+ ) -> None:
Z_tip = ztcf_matrix(zero_q, golfer_params, joint_name="club_tip")
Z_rh = ztcf_matrix(zero_q, golfer_params, joint_name="rh")
# Different joints should give different matrices
diff --git a/src/pendulum_simulator/tests/test_jacobians_golfer.py b/src/pendulum_simulator/tests/test_jacobians_golfer.py
index 7ace63191b..3fbaad7f62 100644
--- a/src/pendulum_simulator/tests/test_jacobians_golfer.py
+++ b/src/pendulum_simulator/tests/test_jacobians_golfer.py
@@ -241,17 +241,17 @@ def test_ellipsoid_data_finite(self):
result = ellipsoids_golfer(q, p)
for name, data in result.items():
assert np.all(np.isfinite(data["jacobian"])), f"{name} Jacobian non-finite"
- assert np.all(np.isfinite(data["singular_values"])), (
- f"{name} singular values non-finite"
- )
- assert np.all(np.isfinite(data["mob_semi_axes"])), (
- f"{name} mobility semi-axes non-finite"
- )
+ assert np.all(
+ np.isfinite(data["singular_values"])
+ ), f"{name} singular values non-finite"
+ assert np.all(
+ np.isfinite(data["mob_semi_axes"])
+ ), f"{name} mobility semi-axes non-finite"
# force_semi_axes may be None at singular configurations
if data["force_semi_axes"] is not None:
- assert np.all(np.isfinite(data["force_semi_axes"])), (
- f"{name} force semi-axes non-finite"
- )
+ assert np.all(
+ np.isfinite(data["force_semi_axes"])
+ ), f"{name} force semi-axes non-finite"
def test_singular_values_descending(self):
"""Singular values should be in descending order."""
@@ -262,7 +262,9 @@ def test_singular_values_descending(self):
for name, data in result.items():
svs = data["singular_values"]
# Check descending order
- assert np.all(np.diff(svs) <= 0), f"{name} singular values not in descending order"
+ assert np.all(
+ np.diff(svs) <= 0
+ ), f"{name} singular values not in descending order"
def test_directions_orthonormal(self):
"""Ellipsoid directions should be orthonormal."""
@@ -275,9 +277,9 @@ def test_directions_orthonormal(self):
# Check columns are unit vectors
for i in range(dirs.shape[1]):
col_norm = np.linalg.norm(dirs[:, i])
- assert np.isclose(col_norm, 1.0, atol=1e-10), (
- f"{name} direction {i} not unit norm"
- )
+ assert np.isclose(
+ col_norm, 1.0, atol=1e-10
+ ), f"{name} direction {i} not unit norm"
def test_semi_axes_positive(self):
"""Semi-axes lengths should be positive."""
diff --git a/src/pendulum_simulator/tests/test_joint_moments.py b/src/pendulum_simulator/tests/test_joint_moments.py
index d96aa27589..47c7b20baf 100644
--- a/src/pendulum_simulator/tests/test_joint_moments.py
+++ b/src/pendulum_simulator/tests/test_joint_moments.py
@@ -23,15 +23,21 @@ class TestCross2D:
def test_unit_vectors(self):
"""x × y = +1 (CCW)."""
- assert cross_2d(np.array([1.0, 0.0]), np.array([0.0, 1.0])) == pytest.approx(1.0)
+ assert cross_2d(np.array([1.0, 0.0]), np.array([0.0, 1.0])) == pytest.approx(
+ 1.0
+ )
def test_antiparallel(self):
"""y × x = -1 (CW)."""
- assert cross_2d(np.array([0.0, 1.0]), np.array([1.0, 0.0])) == pytest.approx(-1.0)
+ assert cross_2d(np.array([0.0, 1.0]), np.array([1.0, 0.0])) == pytest.approx(
+ -1.0
+ )
def test_parallel(self):
"""Parallel vectors → zero cross product."""
- assert cross_2d(np.array([3.0, 0.0]), np.array([5.0, 0.0])) == pytest.approx(0.0)
+ assert cross_2d(np.array([3.0, 0.0]), np.array([5.0, 0.0])) == pytest.approx(
+ 0.0
+ )
def test_wrong_shape_raises(self):
with pytest.raises((ValueError, TypeError), match="r must be shape"):
diff --git a/src/pendulum_simulator/tests/test_main_window.py b/src/pendulum_simulator/tests/test_main_window.py
index d98597c7e3..f5c366cf9a 100644
--- a/src/pendulum_simulator/tests/test_main_window.py
+++ b/src/pendulum_simulator/tests/test_main_window.py
@@ -156,7 +156,9 @@ def get_selection(self) -> Any:
# mock extract_series
mock_extract = MagicMock(side_effect=[([1], "X", "m"), ([2], "Y", "m")])
- monkeypatch.setattr("double_pendulum_golf.data_extractor.extract_series", mock_extract)
+ monkeypatch.setattr(
+ "double_pendulum_golf.data_extractor.extract_series", mock_extract
+ )
# mock PopOutChart
mock_chart_class = MagicMock()
@@ -199,7 +201,9 @@ def get_selection(self) -> Any:
def mock_extract(*args) -> Any:
raise KeyError("bad")
- monkeypatch.setattr("double_pendulum_golf.data_extractor.extract_series", mock_extract)
+ monkeypatch.setattr(
+ "double_pendulum_golf.data_extractor.extract_series", mock_extract
+ )
mock_msg = MagicMock()
monkeypatch.setattr("PyQt6.QtWidgets.QMessageBox.warning", mock_msg)
diff --git a/src/pendulum_simulator/tests/test_model_registry_gaps.py b/src/pendulum_simulator/tests/test_model_registry_gaps.py
index 4d2c839427..f12abb9856 100644
--- a/src/pendulum_simulator/tests/test_model_registry_gaps.py
+++ b/src/pendulum_simulator/tests/test_model_registry_gaps.py
@@ -55,7 +55,9 @@ def test_overwrites_and_warns(self, caplog: pytest.LogCaptureFixture) -> None:
cfg2 = _make_config("Second Version", n_dof=3)
register_model("__test_overwrite__", cfg1)
- with caplog.at_level(logging.WARNING, logger="double_pendulum_golf.model_registry"):
+ with caplog.at_level(
+ logging.WARNING, logger="double_pendulum_golf.model_registry"
+ ):
register_model("__test_overwrite__", cfg2)
assert "Overwriting existing model registration" in caplog.text
@@ -64,7 +66,9 @@ def test_overwrites_and_warns(self, caplog: pytest.LogCaptureFixture) -> None:
def test_no_warn_first_registration(self, caplog: pytest.LogCaptureFixture) -> None:
"""First registration should not warn."""
- with caplog.at_level(logging.WARNING, logger="double_pendulum_golf.model_registry"):
+ with caplog.at_level(
+ logging.WARNING, logger="double_pendulum_golf.model_registry"
+ ):
register_model("__test_first__", _make_config())
assert "Overwriting" not in caplog.text
@@ -90,7 +94,9 @@ def test_import_error_branches(
monkeypatch.setitem(sys.modules, "double_pendulum_golf.physics_triple", None)
monkeypatch.setitem(sys.modules, "double_pendulum_golf.physics_golfer", None)
- with caplog.at_level(logging.DEBUG, logger="double_pendulum_golf.model_registry"):
+ with caplog.at_level(
+ logging.DEBUG, logger="double_pendulum_golf.model_registry"
+ ):
model_registry._register_builtins()
# All 3 modules should fail to import and log at DEBUG level
diff --git a/src/pendulum_simulator/tests/test_native_backend.py b/src/pendulum_simulator/tests/test_native_backend.py
index b346780d38..5261861af9 100644
--- a/src/pendulum_simulator/tests/test_native_backend.py
+++ b/src/pendulum_simulator/tests/test_native_backend.py
@@ -307,7 +307,9 @@ def py_double_mass_matrix(
return [[5.0, 2.0], [2.0, 1.0]]
@staticmethod
- def py_double_gravity_vector(q: list[float], params: tuple[float, ...]) -> list[float]:
+ def py_double_gravity_vector(
+ q: list[float], params: tuple[float, ...]
+ ) -> list[float]:
del q, params
return [3.0, 1.0]
@@ -366,7 +368,9 @@ def py_triple_mass_matrix(
return [[14.0, 8.0, 3.0], [8.0, 5.0, 2.0], [3.0, 2.0, 1.0]]
@staticmethod
- def py_triple_gravity_vector(q: list[float], params: tuple[float, ...]) -> list[float]:
+ def py_triple_gravity_vector(
+ q: list[float], params: tuple[float, ...]
+ ) -> list[float]:
del q, params
return [1.0, 2.0, 3.0]
@@ -399,7 +403,9 @@ def py_triple_forward_kinematics(
mass = native_backend.triple_mass_matrix(0.0, 0.0, triple_params)
gravity = native_backend.triple_gravity_vector(0.0, 0.0, 0.0, triple_params)
- coriolis = native_backend.triple_coriolis_vector(0.0, 0.0, 0.0, 0.0, 0.0, triple_params)
+ coriolis = native_backend.triple_coriolis_vector(
+ 0.0, 0.0, 0.0, 0.0, 0.0, triple_params
+ )
fk = native_backend.triple_forward_kinematics(0.0, 0.0, 0.0, triple_params)
assert mass is not None
@@ -505,7 +511,9 @@ def py_golfer_project_velocity(
q_proj = native_backend.golfer_project_to_constraints(
np.zeros(8), golfer_params, max_iters=5, tol=1e-6
)
- qdot_proj = native_backend.golfer_project_velocity(np.zeros(8), np.zeros(8), golfer_params)
+ qdot_proj = native_backend.golfer_project_velocity(
+ np.zeros(8), np.zeros(8), golfer_params
+ )
assert q_proj is not None
assert qdot_proj is not None
diff --git a/src/pendulum_simulator/tests/test_native_backend_gaps.py b/src/pendulum_simulator/tests/test_native_backend_gaps.py
index 472bccce65..7ac3a2e3cf 100644
--- a/src/pendulum_simulator/tests/test_native_backend_gaps.py
+++ b/src/pendulum_simulator/tests/test_native_backend_gaps.py
@@ -118,7 +118,9 @@ def test_with_zero_b_returns_true(self, golfer_params: GolferParams) -> None:
assert golfer_native_constraint_dynamics_supported(golfer_params) is True
- def test_with_nonzero_b_hub_returns_false(self, golfer_params: GolferParams) -> None:
+ def test_with_nonzero_b_hub_returns_false(
+ self, golfer_params: GolferParams
+ ) -> None:
from double_pendulum_golf.native_backend import (
golfer_native_constraint_dynamics_supported,
)
diff --git a/src/pendulum_simulator/tests/test_optimizer_advanced.py b/src/pendulum_simulator/tests/test_optimizer_advanced.py
index 92f9f588aa..0a39f2d548 100644
--- a/src/pendulum_simulator/tests/test_optimizer_advanced.py
+++ b/src/pendulum_simulator/tests/test_optimizer_advanced.py
@@ -19,7 +19,9 @@ def _has_optimizer() -> bool:
return False
-pytestmark = pytest.mark.skipif(not _has_optimizer(), reason="PyQt6/optimizer not available")
+pytestmark = pytest.mark.skipif(
+ not _has_optimizer(), reason="PyQt6/optimizer not available"
+)
class TestCMAESStep:
@@ -111,7 +113,9 @@ def test_warm_start_advantage(self) -> None:
state_cold, _ = _cmaes_step(state_cold, self._sphere, pop_size=10, rng=rng)
rng_w = np.random.default_rng(42)
for _ in range(20):
- state_warm, _ = _cmaes_step(state_warm, self._sphere, pop_size=10, rng=rng_w)
+ state_warm, _ = _cmaes_step(
+ state_warm, self._sphere, pop_size=10, rng=rng_w
+ )
assert state_warm.best_fitness < state_cold.best_fitness
diff --git a/src/pendulum_simulator/tests/test_optimizer_gpu.py b/src/pendulum_simulator/tests/test_optimizer_gpu.py
index 6ce9db1111..6328e290b9 100644
--- a/src/pendulum_simulator/tests/test_optimizer_gpu.py
+++ b/src/pendulum_simulator/tests/test_optimizer_gpu.py
@@ -97,7 +97,9 @@ def test_gradient_via_autodiff_vs_finite_difference(
# Compute gradient via autodiff
def loss_fn(coeffs):
- return clubhead_speed_objective(coeffs, _PARAMS, state_jax, t_end=0.5, dt=0.01)
+ return clubhead_speed_objective(
+ coeffs, _PARAMS, state_jax, t_end=0.5, dt=0.01
+ )
grad_autodiff = jax.grad(loss_fn)(torque_jax)
@@ -114,7 +116,9 @@ def loss_fn(coeffs):
# Normalize by max absolute value to avoid scale issues
max_grad = np.max(np.abs(grad_fd_np))
if max_grad > 1e-10:
- rel_error = np.linalg.norm(grad_autodiff_np - grad_fd_np) / (max_grad + 1e-12)
+ rel_error = np.linalg.norm(grad_autodiff_np - grad_fd_np) / (
+ max_grad + 1e-12
+ )
assert rel_error < 0.5, f"Relative error in gradient: {rel_error}"
@@ -196,7 +200,9 @@ def test_clubhead_speed_is_positive(
assert float(speed) >= 0.0
@pytest.mark.slow
- def test_clubhead_speed_increases_with_torque(self, initial_state: np.ndarray) -> None:
+ def test_clubhead_speed_increases_with_torque(
+ self, initial_state: np.ndarray
+ ) -> None:
"""Clubhead speed is higher with positive torques."""
state_jax = jnp.array(initial_state)
@@ -245,4 +251,6 @@ def test_fd_gradient_is_finite(
)
grad_np = np.array(grad)
- assert np.all(np.isfinite(grad_np)), f"Gradient has non-finite values: {grad_np}"
+ assert np.all(
+ np.isfinite(grad_np)
+ ), f"Gradient has non-finite values: {grad_np}"
diff --git a/src/pendulum_simulator/tests/test_overlay_state_sync.py b/src/pendulum_simulator/tests/test_overlay_state_sync.py
index af99b0b916..a766eb0ce5 100644
--- a/src/pendulum_simulator/tests/test_overlay_state_sync.py
+++ b/src/pendulum_simulator/tests/test_overlay_state_sync.py
@@ -178,7 +178,9 @@ def test_force_scale_pushed(qapp) -> None:
apply_toolstrip_overlay_state(ts, pw)
- assert pw.calls.get("set_force_scale") == pytest.approx(_expected_scale(ts._sld_force))
+ assert pw.calls.get("set_force_scale") == pytest.approx(
+ _expected_scale(ts._sld_force)
+ )
def test_mob_ellipsoid_scale_pushed(qapp) -> None:
diff --git a/src/pendulum_simulator/tests/test_panel_builders.py b/src/pendulum_simulator/tests/test_panel_builders.py
index 2d64440472..cced2348ff 100644
--- a/src/pendulum_simulator/tests/test_panel_builders.py
+++ b/src/pendulum_simulator/tests/test_panel_builders.py
@@ -181,7 +181,9 @@ def test_build_triple_panel(mock_run, mock_set_perturb, qapp) -> Any:
real_perturb._get_coeffs_for_preset_fn("Default")
- panel.controls.PRESETS = {"Default": ["0", "0", "0", "0", "0", "0", "1.0, 2.0", "3.0", ""]}
+ panel.controls.PRESETS = {
+ "Default": ["0", "0", "0", "0", "0", "0", "1.0, 2.0", "3.0", ""]
+ }
parsed = real_perturb._get_coeffs_for_preset_fn("Default")
assert len(parsed) == 3
diff --git a/src/pendulum_simulator/tests/test_perturbation_analysis.py b/src/pendulum_simulator/tests/test_perturbation_analysis.py
index e0ba5632f5..3eb0382fb2 100644
--- a/src/pendulum_simulator/tests/test_perturbation_analysis.py
+++ b/src/pendulum_simulator/tests/test_perturbation_analysis.py
@@ -119,7 +119,9 @@ def test_defaults(self):
assert cfg.seed is None
def test_custom(self):
- cfg = PerturbationConfig(n_trials=50, noise_type="pink", noise_amplitude=0.2, seed=42)
+ cfg = PerturbationConfig(
+ n_trials=50, noise_type="pink", noise_amplitude=0.2, seed=42
+ )
assert cfg.n_trials == 50
assert cfg.noise_type == "pink"
@@ -199,7 +201,9 @@ def extract_fn(result):
"tip_position_final": np.array([1.0, -0.5]),
}
- results = batch_perturb_and_simulate(base_coeffs, config, simulate_fn, extract_fn)
+ results = batch_perturb_and_simulate(
+ base_coeffs, config, simulate_fn, extract_fn
+ )
assert len(results) == 5
def test_handles_failures_gracefully(self):
@@ -222,7 +226,9 @@ def extract_fn(result):
"tip_position_final": np.array([0.0, 0.0]),
}
- results = batch_perturb_and_simulate(base_coeffs, config, simulate_fn, extract_fn)
+ results = batch_perturb_and_simulate(
+ base_coeffs, config, simulate_fn, extract_fn
+ )
assert len(results) == 2 # 3 trials, 1 failed
@@ -277,7 +283,9 @@ def extract_fn(_result):
"tip_position_final": np.array([0.5, -0.3]),
}
- results = batch_perturb_and_simulate(base_coeffs, config, simulate_fn, extract_fn)
+ results = batch_perturb_and_simulate(
+ base_coeffs, config, simulate_fn, extract_fn
+ )
assert len(results) > 0
for r in results:
assert np.isfinite(r["tip_speed_final"]), f"Non-finite tip_speed: {r}"
diff --git a/src/pendulum_simulator/tests/test_physics.py b/src/pendulum_simulator/tests/test_physics.py
index aaa302a4d8..2a04a2028b 100644
--- a/src/pendulum_simulator/tests/test_physics.py
+++ b/src/pendulum_simulator/tests/test_physics.py
@@ -46,25 +46,29 @@ def test_symmetric_at_arbitrary_angle(self, default_params: PendulumParams) -> N
class TestMassMatrixPositiveDefinite:
"""The mass matrix must be positive definite (all eigenvalues > 0)."""
- def test_positive_definite_at_various_angles(self, default_params: PendulumParams) -> None:
+ def test_positive_definite_at_various_angles(
+ self, default_params: PendulumParams
+ ) -> None:
for phi in np.linspace(-np.pi, np.pi, 50):
M = mass_matrix(phi, default_params)
eigenvalues = np.linalg.eigvalsh(M)
- assert all(ev > 0 for ev in eigenvalues), (
- f"Not positive definite at phi={phi}: eigenvalues={eigenvalues}"
- )
+ assert all(
+ ev > 0 for ev in eigenvalues
+ ), f"Not positive definite at phi={phi}: eigenvalues={eigenvalues}"
class TestMassMatrixCouplingMaximum:
"""Off-diagonal coupling |M12| should be maximized when segments are aligned (phi=0)."""
- def test_coupling_maximized_at_alignment(self, default_params: PendulumParams) -> None:
+ def test_coupling_maximized_at_alignment(
+ self, default_params: PendulumParams
+ ) -> None:
M12_at_zero = abs(mass_matrix(0.0, default_params)[0, 1])
for phi in np.linspace(0.1, np.pi, 30):
M12 = abs(mass_matrix(phi, default_params)[0, 1])
- assert M12 <= M12_at_zero + 1e-10, (
- f"|M12| at phi={phi:.2f} ({M12:.4f}) exceeds value at phi=0 ({M12_at_zero:.4f})"
- )
+ assert (
+ M12 <= M12_at_zero + 1e-10
+ ), f"|M12| at phi={phi:.2f} ({M12:.4f}) exceeds value at phi=0 ({M12_at_zero:.4f})"
class TestMassMatrixDiagonalConstant:
@@ -74,7 +78,9 @@ def test_m22_independent_of_phi(self, default_params: PendulumParams) -> None:
M22_ref = mass_matrix(0.0, default_params)[1, 1]
for phi in np.linspace(-np.pi, np.pi, 30):
M22 = mass_matrix(phi, default_params)[1, 1]
- assert np.isclose(M22, M22_ref), f"M22 changed at phi={phi}: {M22} vs {M22_ref}"
+ assert np.isclose(
+ M22, M22_ref
+ ), f"M22 changed at phi={phi}: {M22} vs {M22_ref}"
def test_m22_equals_expected(self, default_params: PendulumParams) -> None:
"""M22 = m2 * L2^2 for point mass at tip."""
@@ -117,7 +123,9 @@ def test_perpendicular_equal_segments(self, equal_params: PendulumParams) -> Non
class TestCoriolisVector:
"""Tests for the Coriolis/centrifugal force computation."""
- def test_zero_velocity_gives_zero_coriolis(self, default_params: PendulumParams) -> None:
+ def test_zero_velocity_gives_zero_coriolis(
+ self, default_params: PendulumParams
+ ) -> None:
"""No velocity => no velocity-dependent forces."""
C = coriolis_vector(0.5, 0.0, 0.0, default_params)
assert np.allclose(C, [0.0, 0.0])
@@ -322,15 +330,21 @@ def test_full_penetration_no_blend(self):
# pen >= transition → blend=1 → smooth=1 → full penalty
pen = 0.05 # exactly at transition
- result = _hermite_penalty(pen, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0)
+ result = _hermite_penalty(
+ pen, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0
+ )
assert result == pytest.approx(500.0 * 0.05, rel=1e-9)
def test_large_penetration_clamps_blend(self):
from double_pendulum_golf.physics import _hermite_penalty
# pen >> transition → blend clamped at 1 → same as full penalty
- r1 = _hermite_penalty(0.05, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0)
- r2 = _hermite_penalty(1.0, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0)
+ r1 = _hermite_penalty(
+ 0.05, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0
+ )
+ r2 = _hermite_penalty(
+ 1.0, vel=0.0, transition=0.05, stiffness=500.0, damping=0.0
+ )
# Both have blend=1; r2 has larger pen so larger result
assert r2 > r1
@@ -339,9 +353,13 @@ def test_damping_only_when_velocity_into_limit(self):
# vel > 0 means moving into the limit → damping adds
pen = 0.05
- r_into = _hermite_penalty(pen, vel=1.0, transition=0.05, stiffness=0.0, damping=20.0)
+ r_into = _hermite_penalty(
+ pen, vel=1.0, transition=0.05, stiffness=0.0, damping=20.0
+ )
# vel = 0 means no damping contribution
- r_zero = _hermite_penalty(pen, vel=0.0, transition=0.05, stiffness=0.0, damping=20.0)
+ r_zero = _hermite_penalty(
+ pen, vel=0.0, transition=0.05, stiffness=0.0, damping=20.0
+ )
assert r_into > r_zero
@@ -364,7 +382,9 @@ def limits(self):
def test_within_limits_gives_zero(self, limits):
from double_pendulum_golf.physics import joint_limit_torque
- tau = joint_limit_torque(phi=0.0, dphi=0.0, limits=limits, theta1=0.0, dtheta1=0.0)
+ tau = joint_limit_torque(
+ phi=0.0, dphi=0.0, limits=limits, theta1=0.0, dtheta1=0.0
+ )
np.testing.assert_allclose(tau, [0.0, 0.0], atol=1e-12)
def test_exactly_at_lower_phi_limit_gives_zero(self, limits):
@@ -401,9 +421,9 @@ def test_segment_lengths_arbitrary_angle(self, default_params: PendulumParams):
tx, ty = pos["tip"]
wrist_dist = np.hypot(wx - sx, wy - sy)
tip_dist = np.hypot(tx - wx, ty - wy)
- assert abs(wrist_dist - default_params.L1) < 1e-9, (
- f"theta1={theta1:.2f}, phi={phi:.2f}: wrist_dist={wrist_dist:.9f}"
- )
- assert abs(tip_dist - default_params.L2) < 1e-9, (
- f"theta1={theta1:.2f}, phi={phi:.2f}: tip_dist={tip_dist:.9f}"
- )
+ assert (
+ abs(wrist_dist - default_params.L1) < 1e-9
+ ), f"theta1={theta1:.2f}, phi={phi:.2f}: wrist_dist={wrist_dist:.9f}"
+ assert (
+ abs(tip_dist - default_params.L2) < 1e-9
+ ), f"theta1={theta1:.2f}, phi={phi:.2f}: tip_dist={tip_dist:.9f}"
diff --git a/src/pendulum_simulator/tests/test_physics_extended.py b/src/pendulum_simulator/tests/test_physics_extended.py
index c0eed5f10b..2dfb034aad 100644
--- a/src/pendulum_simulator/tests/test_physics_extended.py
+++ b/src/pendulum_simulator/tests/test_physics_extended.py
@@ -211,7 +211,9 @@ def test_shape(self, wide_limits: JointLimitsNDOF) -> None:
assert tau.shape == (2,)
def test_finite(self, wide_limits: JointLimitsNDOF) -> None:
- tau = joint_limit_torque_ndof(np.array([1.0, -0.5]), np.array([0.5, 0.1]), wide_limits)
+ tau = joint_limit_torque_ndof(
+ np.array([1.0, -0.5]), np.array([0.5, 0.1]), wide_limits
+ )
assert np.all(np.isfinite(tau))
@@ -246,7 +248,9 @@ def test_returns_dict(self, params: PendulumParams, rest_state: np.ndarray) -> N
result = joint_velocities(rest_state, params)
assert isinstance(result, dict)
- def test_has_speed_keys(self, params: PendulumParams, rest_state: np.ndarray) -> None:
+ def test_has_speed_keys(
+ self, params: PendulumParams, rest_state: np.ndarray
+ ) -> None:
result = joint_velocities(rest_state, params)
assert "wrist_speed" in result
assert "tip_speed" in result
@@ -281,7 +285,9 @@ def test_returns_dict(self, params: PendulumParams, rest_state: np.ndarray) -> N
result = base_force(rest_state, qddot, params)
assert isinstance(result, dict)
- def test_has_required_keys(self, params: PendulumParams, rest_state: np.ndarray) -> None:
+ def test_has_required_keys(
+ self, params: PendulumParams, rest_state: np.ndarray
+ ) -> None:
qddot = np.zeros(2)
result = base_force(rest_state, qddot, params)
assert "fx" in result
@@ -317,7 +323,9 @@ def test_finite(self, params: PendulumParams, moving_state: np.ndarray) -> None:
qddot = ztcf_accelerations(moving_state, params)
assert np.all(np.isfinite(qddot))
- def test_zero_at_equilibrium(self, params: PendulumParams, rest_state: np.ndarray) -> None:
+ def test_zero_at_equilibrium(
+ self, params: PendulumParams, rest_state: np.ndarray
+ ) -> None:
"""At equilibrium with no velocity, ZTCF accel should be zero."""
qddot = ztcf_accelerations(rest_state, params)
np.testing.assert_allclose(qddot, 0.0, atol=1e-10)
@@ -334,7 +342,9 @@ def test_returns_dict(self, params: PendulumParams, rest_state: np.ndarray) -> N
result = linear_accelerations(rest_state, qddot, params)
assert isinstance(result, dict)
- def test_has_wrist_and_tip(self, params: PendulumParams, rest_state: np.ndarray) -> None:
+ def test_has_wrist_and_tip(
+ self, params: PendulumParams, rest_state: np.ndarray
+ ) -> None:
qddot = np.zeros(2)
result = linear_accelerations(rest_state, qddot, params)
assert "wrist" in result or "ax_wrist" in result or len(result) >= 2
@@ -360,13 +370,17 @@ def test_finite(self, params: PendulumParams, rest_state: np.ndarray) -> None:
E = total_energy(rest_state, params)
assert np.isfinite(E)
- def test_equals_T_plus_V(self, params: PendulumParams, moving_state: np.ndarray) -> None:
+ def test_equals_T_plus_V(
+ self, params: PendulumParams, moving_state: np.ndarray
+ ) -> None:
E = total_energy(moving_state, params)
T = kinetic_energy(moving_state, params)
V = potential_energy(moving_state, params)
assert E == pytest.approx(T + V, rel=1e-9)
- def test_rest_equals_pe_only(self, params: PendulumParams, rest_state: np.ndarray) -> None:
+ def test_rest_equals_pe_only(
+ self, params: PendulumParams, rest_state: np.ndarray
+ ) -> None:
E = total_energy(rest_state, params)
V = potential_energy(rest_state, params)
assert E == pytest.approx(V, abs=1e-10)
diff --git a/src/pendulum_simulator/tests/test_physics_golfer.py b/src/pendulum_simulator/tests/test_physics_golfer.py
index 8274687bcd..23d8c4448b 100644
--- a/src/pendulum_simulator/tests/test_physics_golfer.py
+++ b/src/pendulum_simulator/tests/test_physics_golfer.py
@@ -197,9 +197,9 @@ def test_positive_semi_definite(self, golfer_params: GolferParams) -> None:
q = np.zeros(N_DOF)
M = mass_matrix(q, golfer_params)
eigenvalues = np.linalg.eigvalsh(M)
- assert np.all(eigenvalues >= -1e-10), (
- f"M must be positive semi-definite, got eigenvalues {eigenvalues}"
- )
+ assert np.all(
+ eigenvalues >= -1e-10
+ ), f"M must be positive semi-definite, got eigenvalues {eigenvalues}"
def test_depends_on_configuration(self, golfer_params: GolferParams) -> None:
q1 = np.zeros(N_DOF)
diff --git a/src/pendulum_simulator/tests/test_physics_golfer_jax.py b/src/pendulum_simulator/tests/test_physics_golfer_jax.py
index 9ab09af6b3..5cfabaab46 100644
--- a/src/pendulum_simulator/tests/test_physics_golfer_jax.py
+++ b/src/pendulum_simulator/tests/test_physics_golfer_jax.py
@@ -244,7 +244,9 @@ def test_gravity_vector_shape(self, random_config: np.ndarray) -> None:
G_jax = gravity_vector_jax(q_jax, _PARAMS_JAX)
assert G_jax.shape == (N_DOF,)
- def test_gravity_vector_parity_random_configs(self, random_config: np.ndarray) -> None:
+ def test_gravity_vector_parity_random_configs(
+ self, random_config: np.ndarray
+ ) -> None:
"""JAX gravity vector matches numpy."""
q_jax = jnp.array(random_config)
diff --git a/src/pendulum_simulator/tests/test_physics_native_dbc.py b/src/pendulum_simulator/tests/test_physics_native_dbc.py
index 87c4cf3ad5..c8868f17bf 100644
--- a/src/pendulum_simulator/tests/test_physics_native_dbc.py
+++ b/src/pendulum_simulator/tests/test_physics_native_dbc.py
@@ -202,9 +202,13 @@ def test_no_misleading_fallback_log_for_golfer(self) -> None:
# The double-pendulum path legitimately falls back; the golfer path must
# not claim a fallback that does not exist. Assert the specific stale
# golfer log string is gone.
- assert "golfer mass_matrix call failed (%s), falling back to NumPy" not in source
+ assert (
+ "golfer mass_matrix call failed (%s), falling back to NumPy" not in source
+ )
- @pytest.mark.skipif(not physics_native.HAS_NATIVE, reason="native pendulum_core not built")
+ @pytest.mark.skipif(
+ not physics_native.HAS_NATIVE, reason="native pendulum_core not built"
+ )
def test_construction_succeeds_with_native(self) -> None:
golfer = physics_native.Golfer(**_GOLFER_KWARGS)
assert golfer.use_native is True
diff --git a/src/pendulum_simulator/tests/test_physics_triple.py b/src/pendulum_simulator/tests/test_physics_triple.py
index 302426717e..758d635046 100644
--- a/src/pendulum_simulator/tests/test_physics_triple.py
+++ b/src/pendulum_simulator/tests/test_physics_triple.py
@@ -57,15 +57,17 @@ def test_symmetric_at_zero(self, triple_params: TriplePendulumParams) -> None:
for j in range(3):
assert np.isclose(M[i, j], M[j, i]), f"M[{i},{j}] != M[{j},{i}]"
- def test_symmetric_at_arbitrary_angles(self, triple_params: TriplePendulumParams) -> None:
+ def test_symmetric_at_arbitrary_angles(
+ self, triple_params: TriplePendulumParams
+ ) -> None:
for phi1 in np.linspace(-np.pi, np.pi, 10):
for phi2 in np.linspace(-np.pi, np.pi, 10):
M = mass_matrix_triple(phi1, phi2, triple_params)
for i in range(3):
for j in range(3):
- assert np.isclose(M[i, j], M[j, i]), (
- f"Not symmetric at phi1={phi1}, phi2={phi2}"
- )
+ assert np.isclose(
+ M[i, j], M[j, i]
+ ), f"Not symmetric at phi1={phi1}, phi2={phi2}"
class TestTripleMassMatrixPositiveDefinite:
@@ -79,9 +81,9 @@ def test_positive_definite_at_various_angles(
for phi2 in test_angles:
M = mass_matrix_triple(phi1, phi2, triple_params)
eigenvalues = np.linalg.eigvalsh(M)
- assert all(ev > 0 for ev in eigenvalues), (
- f"Not positive definite at phi1={phi1}, phi2={phi2}"
- )
+ assert all(
+ ev > 0 for ev in eigenvalues
+ ), f"Not positive definite at phi1={phi1}, phi2={phi2}"
class TestTripleCoriolisZeroAtRest:
@@ -159,7 +161,9 @@ def test_eom_produces_valid_state_derivative(
) -> None:
# State: [theta1, phi1, phi2, dtheta1, dphi1, dphi2]
state = np.array([0.1, 0.05, -0.05, 0.0, 0.0, 0.0])
- state_dot = equations_of_motion_triple(state, 0.0, triple_params, triple_torque_func)
+ state_dot = equations_of_motion_triple(
+ state, 0.0, triple_params, triple_torque_func
+ )
assert state_dot.shape == (6,)
assert all(np.isfinite(state_dot)), f"Invalid values: {state_dot}"
@@ -171,7 +175,9 @@ def test_eom_at_rest_at_equilibrium(
) -> None:
# At equilibrium with zero velocity, acceleration should be zero
state = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
- state_dot = equations_of_motion_triple(state, 0.0, triple_params, triple_torque_func)
+ state_dot = equations_of_motion_triple(
+ state, 0.0, triple_params, triple_torque_func
+ )
# Velocities should match input (first 3 elements should be all zeros)
assert np.isclose(state_dot[0], 0.0) # dtheta1
@@ -219,10 +225,14 @@ def test_coriolis_scales_with_velocity_squared(
phi1, phi2 = 0.5, -0.3
dtheta1_small = 0.1
- C_small = coriolis_vector_triple(phi1, phi2, dtheta1_small, 0.1, 0.1, triple_params)
+ C_small = coriolis_vector_triple(
+ phi1, phi2, dtheta1_small, 0.1, 0.1, triple_params
+ )
dtheta1_large = 0.2 # 2x larger
- C_large = coriolis_vector_triple(phi1, phi2, dtheta1_large, 0.1, 0.1, triple_params)
+ C_large = coriolis_vector_triple(
+ phi1, phi2, dtheta1_large, 0.1, 0.1, triple_params
+ )
# The change should not be linear (quadratic in velocity)
ratio = np.linalg.norm(C_large) / np.linalg.norm(C_small)
diff --git a/src/pendulum_simulator/tests/test_physics_triple_extended.py b/src/pendulum_simulator/tests/test_physics_triple_extended.py
index 98848e1aec..43d0714e71 100644
--- a/src/pendulum_simulator/tests/test_physics_triple_extended.py
+++ b/src/pendulum_simulator/tests/test_physics_triple_extended.py
@@ -68,7 +68,9 @@ def moving_state() -> np.ndarray:
class TestMassMatrixComponents:
- def test_returns_dict_with_required_keys(self, params: TriplePendulumParams) -> None:
+ def test_returns_dict_with_required_keys(
+ self, params: TriplePendulumParams
+ ) -> None:
result = mass_matrix_components(0.0, 0.0, params)
assert isinstance(result, dict)
for key in ("M11", "M22", "M33", "M_full"):
@@ -178,7 +180,9 @@ def test_finite_with_motion(
class TestKineticEnergy:
- def test_zero_at_rest(self, params: TriplePendulumParams, rest_state: np.ndarray) -> None:
+ def test_zero_at_rest(
+ self, params: TriplePendulumParams, rest_state: np.ndarray
+ ) -> None:
T = kinetic_energy(rest_state, params)
assert T == pytest.approx(0.0, abs=1e-12)
@@ -187,7 +191,9 @@ def test_positive_with_velocity(self, params: TriplePendulumParams) -> None:
T = kinetic_energy(state, params)
assert T > 0
- def test_scales_quadratically_with_velocity(self, params: TriplePendulumParams) -> None:
+ def test_scales_quadratically_with_velocity(
+ self, params: TriplePendulumParams
+ ) -> None:
"""Doubling velocity should roughly quadruple KE."""
state_slow = np.array([0.0, 0.0, 0.0, 0.5, 0.0, 0.0])
state_fast = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0])
@@ -195,7 +201,9 @@ def test_scales_quadratically_with_velocity(self, params: TriplePendulumParams)
T_fast = kinetic_energy(state_fast, params)
assert T_fast == pytest.approx(4 * T_slow, rel=1e-6)
- def test_finite(self, params: TriplePendulumParams, moving_state: np.ndarray) -> None:
+ def test_finite(
+ self, params: TriplePendulumParams, moving_state: np.ndarray
+ ) -> None:
assert np.isfinite(kinetic_energy(moving_state, params))
@@ -249,7 +257,9 @@ def test_equals_T_plus_V_with_motion(
V = potential_energy(moving_state, params)
assert E == pytest.approx(T + V, rel=1e-8)
- def test_finite(self, params: TriplePendulumParams, moving_state: np.ndarray) -> None:
+ def test_finite(
+ self, params: TriplePendulumParams, moving_state: np.ndarray
+ ) -> None:
assert np.isfinite(total_energy(moving_state, params))
def test_more_than_potential_alone(
diff --git a/src/pendulum_simulator/tests/test_physics_triple_gaps.py b/src/pendulum_simulator/tests/test_physics_triple_gaps.py
index 54b157734c..76f7bcaf2b 100644
--- a/src/pendulum_simulator/tests/test_physics_triple_gaps.py
+++ b/src/pendulum_simulator/tests/test_physics_triple_gaps.py
@@ -43,11 +43,15 @@ def test_with_torque_limits_clamps(
def huge_torque(t):
return (1e6, 1e6, 1e6)
- state_dot = equations_of_motion(state, 0.0, params, huge_torque, torque_limits=limits)
+ state_dot = equations_of_motion(
+ state, 0.0, params, huge_torque, torque_limits=limits
+ )
assert state_dot.shape == (6,)
assert np.all(np.isfinite(state_dot))
- def test_with_large_limits_passes_through(self, params: TriplePendulumParams) -> None:
+ def test_with_large_limits_passes_through(
+ self, params: TriplePendulumParams
+ ) -> None:
"""With infinite limits, torques pass through unchanged."""
state = np.array([0.1, 0.05, -0.05, 0.0, 0.0, 0.0])
limits = np.array([np.inf, np.inf, np.inf])
@@ -55,7 +59,9 @@ def test_with_large_limits_passes_through(self, params: TriplePendulumParams) ->
def tau_fn(t):
return (5.0, -3.0, 2.0)
- state_dot = equations_of_motion(state, 0.0, params, tau_fn, torque_limits=limits)
+ state_dot = equations_of_motion(
+ state, 0.0, params, tau_fn, torque_limits=limits
+ )
assert state_dot.shape == (6,)
assert np.all(np.isfinite(state_dot))
@@ -64,7 +70,9 @@ def test_no_torque_limits_same_as_none(
) -> None:
"""Without limits, result should match None path."""
state = np.array([0.1, 0.05, -0.05, 0.0, 0.0, 0.0])
- sd_no_limits = equations_of_motion(state, 0.0, params, zero_torque, torque_limits=None)
+ sd_no_limits = equations_of_motion(
+ state, 0.0, params, zero_torque, torque_limits=None
+ )
assert np.all(np.isfinite(sd_no_limits))
diff --git a/src/pendulum_simulator/tests/test_side_panel_tabs.py b/src/pendulum_simulator/tests/test_side_panel_tabs.py
index cdcaee6c6b..b5e8cb304a 100644
--- a/src/pendulum_simulator/tests/test_side_panel_tabs.py
+++ b/src/pendulum_simulator/tests/test_side_panel_tabs.py
@@ -98,9 +98,9 @@ def test_each_panel_is_wrapped_in_scroll_area(qapp) -> Any:
tabs.add_panel("Plots", QLabel("b"))
for i in range(tabs.count()):
wrapper = tabs.widget(i)
- assert isinstance(wrapper, QScrollArea), (
- f"Tab {i} is {type(wrapper).__name__}, expected QScrollArea"
- )
+ assert isinstance(
+ wrapper, QScrollArea
+ ), f"Tab {i} is {type(wrapper).__name__}, expected QScrollArea"
def test_added_widget_reachable_through_panel_widget(qapp) -> Any:
@@ -184,7 +184,9 @@ def test_restore_state_with_no_saved_value_is_noop(qapp) -> Any:
def test_restore_state_with_obsolete_label_falls_back(qapp) -> Any:
"""Saved label that no longer exists keeps the default tab."""
- QSettings("D-sorganization", "PendulumSimulator").setValue(_TEST_KEY, "ObsoleteLabel")
+ QSettings("D-sorganization", "PendulumSimulator").setValue(
+ _TEST_KEY, "ObsoleteLabel"
+ )
tabs = SidePanelTabs(settings_key=_TEST_KEY)
tabs.add_panel("Setup", QLabel("a"))
tabs.add_panel("Plots", QLabel("b"))
diff --git a/src/pendulum_simulator/tests/test_simulation.py b/src/pendulum_simulator/tests/test_simulation.py
index 97beaf2c31..5e10f3aa97 100644
--- a/src/pendulum_simulator/tests/test_simulation.py
+++ b/src/pendulum_simulator/tests/test_simulation.py
@@ -134,7 +134,9 @@ def test_native_backend_integration(self, default_params: PendulumParams) -> Non
assert len(result.t) == 10
assert np.isclose(result.t[1] - result.t[0], 0.1)
- def test_native_backend_too_few_points(self, default_params: PendulumParams) -> None:
+ def test_native_backend_too_few_points(
+ self, default_params: PendulumParams
+ ) -> None:
import unittest.mock as mock
with (
@@ -209,13 +211,16 @@ def test_energy_conserved_free_pendulum(
)
E0 = total_energy(result.states[0], equal_params)
energies = np.array(
- [total_energy(result.states[i], equal_params) for i in range(result.n_steps)]
+ [
+ total_energy(result.states[i], equal_params)
+ for i in range(result.n_steps)
+ ]
)
max_drift = np.max(np.abs(energies - E0))
relative_drift = max_drift / abs(E0) if abs(E0) > 1e-10 else max_drift
- assert relative_drift < 1e-3, (
- f"Energy drift {relative_drift:.2e} exceeds 0.1% threshold"
- )
+ assert (
+ relative_drift < 1e-3
+ ), f"Energy drift {relative_drift:.2e} exceeds 0.1% threshold"
class TestSimulationAccessors:
diff --git a/src/pendulum_simulator/tests/test_simulation_gaps.py b/src/pendulum_simulator/tests/test_simulation_gaps.py
index 7c3ccdd151..3a515ead95 100644
--- a/src/pendulum_simulator/tests/test_simulation_gaps.py
+++ b/src/pendulum_simulator/tests/test_simulation_gaps.py
@@ -124,7 +124,9 @@ def golfer_params() -> GolferParams:
class TestGolferSimulationWithJointLimits:
- def test_limits_code_path_via_direct_call(self, golfer_params: GolferParams) -> None:
+ def test_limits_code_path_via_direct_call(
+ self, golfer_params: GolferParams
+ ) -> None:
"""Directly test the limits branch in the ode_rhs closure.
Instead of running the full simulation (which can hit singular matrices
@@ -186,7 +188,9 @@ def test_simulation_runs_below_abort_threshold(
) -> None:
"""Normal simulation should not trigger constraint abort logging."""
initial_state = np.zeros(2 * N_DOF)
- with caplog.at_level(logging.WARNING, logger="double_pendulum_golf.simulation_golfer"):
+ with caplog.at_level(
+ logging.WARNING, logger="double_pendulum_golf.simulation_golfer"
+ ):
result = run_golfer_sim(
golfer_params,
initial_state,
diff --git a/src/pendulum_simulator/tests/test_simulation_golfer.py b/src/pendulum_simulator/tests/test_simulation_golfer.py
index 80bb734b21..17d6cf47c3 100644
--- a/src/pendulum_simulator/tests/test_simulation_golfer.py
+++ b/src/pendulum_simulator/tests/test_simulation_golfer.py
@@ -100,7 +100,9 @@ def test_constant_torque(self) -> None:
assert result == (1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0)
def test_linear_torque(self) -> None:
- tf = make_polynomial_torque([0.0, 1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0])
+ tf = make_polynomial_torque(
+ [0.0, 1.0], [0.0], [0.0], [0.0], [0.0], [0.0], [0.0]
+ )
result = tf(2.0)
assert abs(result[0] - 2.0) < 1e-10
@@ -121,7 +123,9 @@ def test_states_shape(self, sim_result: GolferSimulationResult) -> None:
assert sim_result.states.shape[1] == 2 * N_DOF
def test_time_monotonic(self, sim_result: GolferSimulationResult) -> None:
- assert np.all(np.diff(sim_result.t) > 0), "Time must be monotonically increasing"
+ assert np.all(
+ np.diff(sim_result.t) > 0
+ ), "Time must be monotonically increasing"
def test_constraint_bounded(self, sim_result: GolferSimulationResult) -> None:
for i in range(sim_result.n_steps):
@@ -154,16 +158,20 @@ def test_run_with_joint_limits(self) -> None:
class TestConstraintViolationPostcondition:
"""Constraint monitoring postcondition: drift must stay within abort threshold."""
- def test_violation_below_abort_threshold(self, sim_result: GolferSimulationResult) -> None:
+ def test_violation_below_abort_threshold(
+ self, sim_result: GolferSimulationResult
+ ) -> None:
"""All trajectory steps must have constraint violation below abort threshold."""
abort_tol = 1e-2
for i in range(sim_result.n_steps):
v = constraint_violation(sim_result.states[i], _GOLFER_PARAMS)
- assert v < abort_tol, (
- f"Constraint violation {v:.3e} at step {i} exceeds abort threshold {abort_tol:.3e}"
- )
+ assert (
+ v < abort_tol
+ ), f"Constraint violation {v:.3e} at step {i} exceeds abort threshold {abort_tol:.3e}"
- def test_violation_finite_at_all_steps(self, sim_result: GolferSimulationResult) -> None:
+ def test_violation_finite_at_all_steps(
+ self, sim_result: GolferSimulationResult
+ ) -> None:
"""Constraint violation must be finite at every trajectory step."""
for i in range(sim_result.n_steps):
v = constraint_violation(sim_result.states[i], _GOLFER_PARAMS)
@@ -220,7 +228,9 @@ def test_mass_matrix_at(self, sim_result: GolferSimulationResult) -> None:
M = sim_result.mass_matrix_at(0)
assert M.shape == (N_DOF, N_DOF)
- def test_all_positions_and_energies(self, sim_result: GolferSimulationResult) -> None:
+ def test_all_positions_and_energies(
+ self, sim_result: GolferSimulationResult
+ ) -> None:
positions = sim_result.all_positions()
energies = sim_result.all_energies()
assert len(positions) == sim_result.n_steps
diff --git a/src/pendulum_simulator/tests/test_simulation_golfer_drift.py b/src/pendulum_simulator/tests/test_simulation_golfer_drift.py
index 9a10f2f8ba..100266389e 100644
--- a/src/pendulum_simulator/tests/test_simulation_golfer_drift.py
+++ b/src/pendulum_simulator/tests/test_simulation_golfer_drift.py
@@ -55,7 +55,9 @@ def test_warn_during_integration_line266(
initial_state = np.concatenate([q0, np.zeros(N_DOF)])
# Always return a value above warn threshold → exercises line 266
- with caplog.at_level(logging.WARNING, logger="double_pendulum_golf.simulation_golfer"):
+ with caplog.at_level(
+ logging.WARNING, logger="double_pendulum_golf.simulation_golfer"
+ ):
with patch(
"double_pendulum_golf.simulation_golfer.constraint_violation",
return_value=1e-3, # > _CONSTRAINT_WARN_TOL (1e-4)
@@ -82,7 +84,9 @@ def test_abort_threshold_log_line296(
initial_state = np.concatenate([q0, np.zeros(N_DOF)])
# Always return a value above abort threshold
- with caplog.at_level(logging.ERROR, logger="double_pendulum_golf.simulation_golfer"):
+ with caplog.at_level(
+ logging.ERROR, logger="double_pendulum_golf.simulation_golfer"
+ ):
with patch(
"double_pendulum_golf.simulation_golfer.constraint_violation",
return_value=0.5, # >> _CONSTRAINT_ABORT_TOL (1e-2)
@@ -108,7 +112,9 @@ def test_warn_threshold_postcondition_line302(
initial_state = np.concatenate([q0, np.zeros(N_DOF)])
# Always return a value between warn and abort thresholds
- with caplog.at_level(logging.WARNING, logger="double_pendulum_golf.simulation_golfer"):
+ with caplog.at_level(
+ logging.WARNING, logger="double_pendulum_golf.simulation_golfer"
+ ):
with patch(
"double_pendulum_golf.simulation_golfer.constraint_violation",
return_value=5e-3, # > WARN (1e-4), < ABORT (1e-2)
diff --git a/src/pendulum_simulator/tests/test_simulation_golfer_extended.py b/src/pendulum_simulator/tests/test_simulation_golfer_extended.py
index 16bef8e3d8..1098ea3f52 100644
--- a/src/pendulum_simulator/tests/test_simulation_golfer_extended.py
+++ b/src/pendulum_simulator/tests/test_simulation_golfer_extended.py
@@ -143,7 +143,9 @@ def test_constraint_forces_at_finite(self, result: GolferSimulationResult) -> No
cf = result.constraint_forces_at(0)
assert np.all(np.isfinite(cf))
- def test_constraint_violation_at_finite(self, result: GolferSimulationResult) -> None:
+ def test_constraint_violation_at_finite(
+ self, result: GolferSimulationResult
+ ) -> None:
cv = result.constraint_violation_at(0)
assert np.isfinite(cv)
@@ -184,7 +186,9 @@ def test_friction_torques_at_shape(self, result: GolferSimulationResult) -> None
tf = result.friction_torques_at(0)
assert tf.shape == (N_DOF,)
- def test_friction_torques_zero_at_rest(self, result: GolferSimulationResult) -> None:
+ def test_friction_torques_zero_at_rest(
+ self, result: GolferSimulationResult
+ ) -> None:
"""At zero velocity, friction should be zero."""
tf = result.friction_torques_at(0)
np.testing.assert_allclose(tf, 0.0, atol=1e-14)
diff --git a/src/pendulum_simulator/tests/test_simulation_panel.py b/src/pendulum_simulator/tests/test_simulation_panel.py
index d94865ce5b..dd28724c74 100644
--- a/src/pendulum_simulator/tests/test_simulation_panel.py
+++ b/src/pendulum_simulator/tests/test_simulation_panel.py
@@ -264,7 +264,9 @@ def test_export_data(qapp, mock_sim_kwargs, tmp_path) -> Any:
panel = SimulationPanel(**mock_sim_kwargs)
# show message if no result
- with patch("double_pendulum_golf.gui.simulation_panel.QMessageBox.information") as info:
+ with patch(
+ "double_pendulum_golf.gui.simulation_panel.QMessageBox.information"
+ ) as info:
panel._on_export_data()
info.assert_called_once()
@@ -358,7 +360,9 @@ def test_apply_optimized_coefficients(qapp, mock_sim_kwargs) -> Any:
panel_triple.controls.inp_tau_shoulder = MagicMock()
panel_triple.controls.inp_tau_elbow = MagicMock()
panel_triple.controls.inp_tau_wrist = MagicMock()
- panel_triple._apply_optimized_coefficients({"coeffs": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]})
+ panel_triple._apply_optimized_coefficients(
+ {"coeffs": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]}
+ )
# Test golfer
mock_sim_kwargs["controls"] = MockControlsGolfer()
@@ -374,7 +378,9 @@ def test_patched_on_run_optimizer(qapp, mock_sim_kwargs) -> Any:
panel = SimulationPanel(**mock_sim_kwargs)
panel.optimizer.bind_objective_builder.assert_called_once()
- params_getter, objective_builder = panel.optimizer.bind_objective_builder.call_args[0]
+ params_getter, objective_builder = panel.optimizer.bind_objective_builder.call_args[
+ 0
+ ]
assert params_getter is panel.controls.get_params
assert objective_builder is panel.objective_builder
@@ -426,7 +432,9 @@ def test_plots_tab_present_when_torque_history_supplied(qapp, mock_sim_kwargs) -
panel = SimulationPanel(**mock_sim_kwargs)
labels = panel._side_tabs.panel_labels()
assert SimulationPanel.TAB_PLOTS in labels
- assert panel._side_tabs.panel_widget(SimulationPanel.TAB_PLOTS) is panel.torque_history
+ assert (
+ panel._side_tabs.panel_widget(SimulationPanel.TAB_PLOTS) is panel.torque_history
+ )
def test_plots_tab_absent_when_torque_history_omitted(qapp, mock_sim_kwargs) -> Any:
diff --git a/src/pendulum_simulator/tests/test_simulation_triple.py b/src/pendulum_simulator/tests/test_simulation_triple.py
index 8280e633ac..a2ea8c4df1 100644
--- a/src/pendulum_simulator/tests/test_simulation_triple.py
+++ b/src/pendulum_simulator/tests/test_simulation_triple.py
@@ -142,7 +142,9 @@ def test_energy_conserved_free_pendulum(
and that energy drift stays below 2% for a 1-second free-pendulum run.
The 2% bound is appropriate for DOP853 on a chaotic triple pendulum.
"""
- state0 = np.array([np.radians(45), np.radians(30), np.radians(-15), 0.0, 0.0, 0.0])
+ state0 = np.array(
+ [np.radians(45), np.radians(30), np.radians(-15), 0.0, 0.0, 0.0]
+ )
result = run_simulation(
triple_params,
state0,
diff --git a/src/pendulum_simulator/tests/test_simulation_triple_extended.py b/src/pendulum_simulator/tests/test_simulation_triple_extended.py
index b592b9d3ba..fb6e41c056 100644
--- a/src/pendulum_simulator/tests/test_simulation_triple_extended.py
+++ b/src/pendulum_simulator/tests/test_simulation_triple_extended.py
@@ -46,7 +46,9 @@ def result(
) -> TripleSimulationResult:
"""Run a short simulation and cache the result for all tests in the module."""
initial_state = np.array([0.1, 0.05, -0.05, 0.0, 0.0, 0.0])
- return run_simulation(params, initial_state, t_end=0.1, torque_func=torque_func, dt=0.01)
+ return run_simulation(
+ params, initial_state, t_end=0.1, torque_func=torque_func, dt=0.01
+ )
class TestRunSimulation:
diff --git a/src/pendulum_simulator/tests/test_swing_comparison_dialog.py b/src/pendulum_simulator/tests/test_swing_comparison_dialog.py
index 39b77ec6ed..74abe147b5 100644
--- a/src/pendulum_simulator/tests/test_swing_comparison_dialog.py
+++ b/src/pendulum_simulator/tests/test_swing_comparison_dialog.py
@@ -270,7 +270,9 @@ def test_run_flow(self, dialog):
}
dialog._on_preset_done("Preset A", summary)
- with patch("double_pendulum_golf.gui.swing_comparison_dialog._HAS_MPL", False):
+ with patch(
+ "double_pendulum_golf.gui.swing_comparison_dialog._HAS_MPL", False
+ ):
dialog._on_all_done([("Preset A", summary)])
from double_pendulum_golf.gui.swing_comparison_dialog import _HAS_MPL
@@ -300,7 +302,9 @@ def test_export(self, dialog, tmp_path):
dialog._results = [("Preset A", summary)]
# no path
- with patch("PyQt6.QtWidgets.QFileDialog.getSaveFileName", return_value=("", "")):
+ with patch(
+ "PyQt6.QtWidgets.QFileDialog.getSaveFileName", return_value=("", "")
+ ):
dialog._on_export()
csv_file = tmp_path / "test.csv"
diff --git a/src/pendulum_simulator/tests/test_toolstrip_elements.py b/src/pendulum_simulator/tests/test_toolstrip_elements.py
index 896edcad1f..5d5971351b 100644
--- a/src/pendulum_simulator/tests/test_toolstrip_elements.py
+++ b/src/pendulum_simulator/tests/test_toolstrip_elements.py
@@ -65,19 +65,19 @@ class TestPlaybackSlider:
def test_frame_slider_exists(self, toolstrip: ToolStrip) -> None:
"""ToolStrip must have a _frame_slider attribute that is a QSlider."""
- assert hasattr(toolstrip, "_frame_slider"), (
- "ToolStrip is missing _frame_slider attribute"
- )
- assert isinstance(toolstrip._frame_slider, QSlider), (
- f"_frame_slider is {type(toolstrip._frame_slider)}, expected QSlider"
- )
+ assert hasattr(
+ toolstrip, "_frame_slider"
+ ), "ToolStrip is missing _frame_slider attribute"
+ assert isinstance(
+ toolstrip._frame_slider, QSlider
+ ), f"_frame_slider is {type(toolstrip._frame_slider)}, expected QSlider"
def test_frame_slider_is_child(self, toolstrip: ToolStrip) -> None:
"""Frame slider must be a descendant widget of the ToolStrip."""
all_sliders = toolstrip.findChildren(QSlider)
- assert toolstrip._frame_slider in all_sliders, (
- "Frame slider is not a child widget of ToolStrip"
- )
+ assert (
+ toolstrip._frame_slider in all_sliders
+ ), "Frame slider is not a child widget of ToolStrip"
def test_frame_slider_has_minimum_width(self, toolstrip: ToolStrip) -> None:
"""Frame slider must have a minimum width >= 200px for visibility."""
@@ -131,7 +131,9 @@ def test_moment_of_force_checkbox_exists(self, toolstrip: ToolStrip) -> None:
def test_sum_moments_checkbox_exists(self, toolstrip: ToolStrip) -> None:
"""ToolStrip must have a chk_sum_moments checkbox."""
- assert hasattr(toolstrip, "chk_sum_moments"), "ToolStrip missing chk_sum_moments"
+ assert hasattr(
+ toolstrip, "chk_sum_moments"
+ ), "ToolStrip missing chk_sum_moments"
assert isinstance(toolstrip.chk_sum_moments, QCheckBox)
def test_torque_signal_connected(self, toolstrip: ToolStrip) -> None:
@@ -166,15 +168,15 @@ class TestNoGravityCheckbox:
def test_no_gravity_checkbox_in_toolstrip(self, toolstrip: ToolStrip) -> None:
"""ToolStrip must NOT have a chk_gravity attribute."""
- assert not hasattr(toolstrip, "chk_gravity"), (
- "chk_gravity still exists in ToolStrip — it must be removed (#1209)"
- )
+ assert not hasattr(
+ toolstrip, "chk_gravity"
+ ), "chk_gravity still exists in ToolStrip — it must be removed (#1209)"
def test_no_gravity_toggled_signal(self, toolstrip: ToolStrip) -> None:
"""ToolStrip must NOT have gravity_toggled signal."""
- assert not hasattr(toolstrip, "gravity_toggled"), (
- "gravity_toggled signal still exists — must be removed (#1209)"
- )
+ assert not hasattr(
+ toolstrip, "gravity_toggled"
+ ), "gravity_toggled signal still exists — must be removed (#1209)"
# ---------------------------------------------------------------------------
diff --git a/src/pendulum_simulator/tests/test_torque_utils.py b/src/pendulum_simulator/tests/test_torque_utils.py
index 2d9aa7a3c0..bd4fd4dcfb 100644
--- a/src/pendulum_simulator/tests/test_torque_utils.py
+++ b/src/pendulum_simulator/tests/test_torque_utils.py
@@ -57,7 +57,9 @@ def test_zero_joints_raises(self):
def test_empty_coefficients_raises(self):
"""Each joint needs at least one coefficient."""
- with pytest.raises((ValueError, TypeError), match="Need at least one coefficient"):
+ with pytest.raises(
+ (ValueError, TypeError), match="Need at least one coefficient"
+ ):
make_polynomial_torque([])
def test_returns_tuple(self):
diff --git a/src/pendulum_simulator/tests/test_ui_enhancements.py b/src/pendulum_simulator/tests/test_ui_enhancements.py
index 4bfd4aea13..2ade43aeaa 100644
--- a/src/pendulum_simulator/tests/test_ui_enhancements.py
+++ b/src/pendulum_simulator/tests/test_ui_enhancements.py
@@ -131,7 +131,9 @@ def test_hub_rotates_correctly(self, golfer_params: GolferParams) -> None:
assert pos["hub"][0] < 0, "Hub should be on left side at π/2"
assert abs(pos["hub"][1]) < 1e-10
- def test_analytical_jacobians_match_numerical(self, golfer_params: GolferParams) -> None:
+ def test_analytical_jacobians_match_numerical(
+ self, golfer_params: GolferParams
+ ) -> None:
"""Analytical Jacobians must match numerical finite-diff after hub reversal."""
rng = np.random.default_rng(42)
eps = 1e-7
@@ -149,9 +151,9 @@ def test_analytical_jacobians_match_numerical(self, golfer_params: GolferParams)
J_hub_num[0, j] = (fkp["hub"][0] - fk0["hub"][0]) / eps
J_hub_num[1, j] = (fkp["hub"][1] - fk0["hub"][1]) / eps
- assert np.allclose(jacs["hub"], J_hub_num, atol=1e-4), (
- f"Hub Jacobian mismatch:\nAnalytical:\n{jacs['hub']}\nNumerical:\n{J_hub_num}"
- )
+ assert np.allclose(
+ jacs["hub"], J_hub_num, atol=1e-4
+ ), f"Hub Jacobian mismatch:\nAnalytical:\n{jacs['hub']}\nNumerical:\n{J_hub_num}"
def test_all_analytical_jacobians_match_numerical(
self, golfer_params: GolferParams
@@ -253,9 +255,9 @@ def test_scapula_position_is_at_bar_endpoint(
# Scapula position should be at the original shoulder bar endpoint
rscap = np.array(pos_scap["rscap"])
rs_orig = np.array(pos_no["rs"])
- assert np.allclose(rscap, rs_orig, atol=1e-10), (
- "Scapula joint should be at original shoulder bar endpoint"
- )
+ assert np.allclose(
+ rscap, rs_orig, atol=1e-10
+ ), "Scapula joint should be at original shoulder bar endpoint"
def test_mass_matrix_still_valid_with_scapula(
self,
@@ -310,9 +312,9 @@ def test_tilt_reduces_potential_energy(self, golfer_params: GolferParams) -> Non
V_tilted = potential_energy_from_q(q, params_tilted)
# PE should be smaller with reduced gravity
- assert abs(V_tilted) < abs(V_full), (
- f"Tilted PE ({V_tilted}) should be smaller than full ({V_full})"
- )
+ assert abs(V_tilted) < abs(
+ V_full
+ ), f"Tilted PE ({V_tilted}) should be smaller than full ({V_full})"
# ---------------------------------------------------------------------------
diff --git a/src/pendulum_simulator/tests/test_ui_polish_fixes.py b/src/pendulum_simulator/tests/test_ui_polish_fixes.py
index dd3bfe4427..eada9fddd1 100644
--- a/src/pendulum_simulator/tests/test_ui_polish_fixes.py
+++ b/src/pendulum_simulator/tests/test_ui_polish_fixes.py
@@ -125,9 +125,9 @@ def test_each_label_has_a_visible_symbol_prefix(self) -> None:
assert stripped, f"Empty label: {label!r}"
first = stripped[0]
# First non-space char must be non-ASCII (a symbol/icon)
- assert not first.isascii(), (
- f"Label {label!r} should start with a symbol prefix, not {first!r}"
- )
+ assert (
+ not first.isascii()
+ ), f"Label {label!r} should start with a symbol prefix, not {first!r}"
# ──────────────────────────────────────────────────────────────────────
diff --git a/src/pendulum_simulator/tests/test_unit_converter.py b/src/pendulum_simulator/tests/test_unit_converter.py
index 40440f99d9..28b80001a3 100644
--- a/src/pendulum_simulator/tests/test_unit_converter.py
+++ b/src/pendulum_simulator/tests/test_unit_converter.py
@@ -82,7 +82,9 @@ def test_imperial_foot_pound_units_use_shared_constants() -> None:
prefs = UnitPreferences()
prefs.set_unit(UnitCategory.TORQUE, "lbf·ft")
- assert to_si(1.0, UnitCategory.TORQUE, prefs) == pytest.approx(FOOT_POUND_TO_NEWTON_METER)
+ assert to_si(1.0, UnitCategory.TORQUE, prefs) == pytest.approx(
+ FOOT_POUND_TO_NEWTON_METER
+ )
assert from_si(
to_si(1.0, UnitCategory.TORQUE, prefs), UnitCategory.TORQUE, prefs
) == pytest.approx(1.0, rel=1e-12)
diff --git a/src/pendulum_simulator/tests/test_v2_comprehensive.py b/src/pendulum_simulator/tests/test_v2_comprehensive.py
index 72e73020ec..a1bc8dc4fe 100644
--- a/src/pendulum_simulator/tests/test_v2_comprehensive.py
+++ b/src/pendulum_simulator/tests/test_v2_comprehensive.py
@@ -268,9 +268,9 @@ def zero_torque(t: float) -> tuple[float, float, float]:
E0 = total_energy(state0, params)
E_final = total_energy(result.states[-1], params)
# Energy should be conserved within integration tolerance
- assert abs(E_final - E0) / max(abs(E0), 1e-10) < 0.01, (
- f"Energy drift: E0={E0:.4f}, E_final={E_final:.4f}"
- )
+ assert (
+ abs(E_final - E0) / max(abs(E0), 1e-10) < 0.01
+ ), f"Energy drift: E0={E0:.4f}, E_final={E_final:.4f}"
class TestUnitConversionModule:
@@ -492,4 +492,6 @@ def test_no_print_statements_in_physics_triple(self) -> None:
source = inspect.getsource(phys_t)
matches = re.findall(r"^\s*print\s*\(", source, re.MULTILINE)
- assert len(matches) == 0, f"Found {len(matches)} print() calls in physics_triple.py"
+ assert (
+ len(matches) == 0
+ ), f"Found {len(matches)} print() calls in physics_triple.py"
diff --git a/src/python/src/utils/error_handling.py b/src/python/src/utils/error_handling.py
index 4dac92801c..142246417b 100644
--- a/src/python/src/utils/error_handling.py
+++ b/src/python/src/utils/error_handling.py
@@ -92,7 +92,9 @@ def safe_execute(
"""
try:
return func(*args, **kwargs)
- except Exception as e: # noqa: BLE001 — intentional catch-all; safe_execute must not propagate
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — intentional catch-all; safe_execute must not propagate
if log_error:
logger.error(f"Error executing {func.__name__}: {e}")
return default
diff --git a/src/python/tests/test_python_dbc_lod.py b/src/python/tests/test_python_dbc_lod.py
index 292daecc2c..0cdc88dd5a 100644
--- a/src/python/tests/test_python_dbc_lod.py
+++ b/src/python/tests/test_python_dbc_lod.py
@@ -119,7 +119,9 @@ def _import_help_handlers() -> Any:
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
- except Exception as e: # noqa: BLE001 — test isolation: any import failure skips suite
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — test isolation: any import failure skips suite
pytest.skip(f"help_system not importable: {e}")
diff --git a/src/rotation_converter/ui/pyqt6/main_window.py b/src/rotation_converter/ui/pyqt6/main_window.py
index f9d12248de..d372f18579 100644
--- a/src/rotation_converter/ui/pyqt6/main_window.py
+++ b/src/rotation_converter/ui/pyqt6/main_window.py
@@ -156,7 +156,9 @@ def _get_plot_colors() -> dict[str, Any]:
"surface": colors.get("group_bg", _DARK_SURFACE),
"axes": CHART_COLORS[:3] if CHART_COLORS else _AXIS_COLORS,
}
- except Exception: # noqa: BLE001 — theme import is optional; fall back to defaults
+ except (
+ Exception
+ ): # noqa: BLE001 — theme import is optional; fall back to defaults
pass
return {
"bg": _DARK_BG,
@@ -338,7 +340,9 @@ def _update_outputs(self) -> None:
rot = Rotation.from_rotation_matrix(R)
else:
return
- except Exception as e: # noqa: BLE001 — user input can raise any error; display it
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — user input can raise any error; display it
self._output_text.setPlainText(f"Error: {e}")
return
@@ -368,7 +372,9 @@ def _update_main_result(self, rot: Rotation) -> None:
else:
res = ""
self._main_result.setText(res)
- except Exception as e: # noqa: BLE001 — rotation conversion may raise any arithmetic error
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — rotation conversion may raise any arithmetic error
self._main_result.setText(f"Error: {e}")
def _display_all(self, rot: Rotation, conv: str) -> None:
@@ -390,7 +396,9 @@ def _display_all(self, rot: Rotation, conv: str) -> None:
e = rot.as_euler(c)
marker = " ◀" if c == conv else ""
lines.append(f" {c}: {e[0]: .6f} {e[1]: .6f} {e[2]: .6f}{marker}")
- except Exception: # noqa: BLE001 — Euler conversion may fail for degenerate rotations
+ except (
+ Exception
+ ): # noqa: BLE001 — Euler conversion may fail for degenerate rotations
lines.append(f" {c}: (error)")
lines += [
"",
@@ -573,7 +581,9 @@ def _update(self) -> None:
T = RigidTransform.from_matrix(v.reshape(4, 4), source=src, target=tgt)
else:
return
- except Exception as e: # noqa: BLE001 — user input can raise any error; display it
+ except (
+ Exception
+ ) as e: # noqa: BLE001 — user input can raise any error; display it
self._tf_output.setPlainText(f"Error: {e}")
return
@@ -631,7 +641,9 @@ def _display_transform(self, T: RigidTransform) -> None:
f" pitch: {screw['pitch']:.6f}",
f" theta: {screw['theta']:.6f} rad",
]
- except Exception: # noqa: BLE001 — screw decomposition is optional display; skip on error
+ except (
+ Exception
+ ): # noqa: BLE001 — screw decomposition is optional display; skip on error
pass
self._tf_output.setPlainText("\n".join(lines))
diff --git a/src/rotation_converter/ui/pyqt6/reference_frame_tab.py b/src/rotation_converter/ui/pyqt6/reference_frame_tab.py
index 560979e028..31b46f3535 100644
--- a/src/rotation_converter/ui/pyqt6/reference_frame_tab.py
+++ b/src/rotation_converter/ui/pyqt6/reference_frame_tab.py
@@ -145,7 +145,9 @@ def _compute(self) -> None:
self._results.setPlainText(json.dumps(result.results, indent=2))
self._markdown.setPlainText(result.explanation_markdown)
self._latex.setPlainText(result.explanation_latex)
- except Exception as error: # noqa: BLE001 — user input can raise any error; display it
+ except (
+ Exception
+ ) as error: # noqa: BLE001 — user input can raise any error; display it
self._results.setPlainText(f"Error: {error}")
self._markdown.clear()
self._latex.clear()
diff --git a/src/rrt_path_planner/python/src/star_wars_rrt.py b/src/rrt_path_planner/python/src/star_wars_rrt.py
index 611793c819..f0537eb8e4 100644
--- a/src/rrt_path_planner/python/src/star_wars_rrt.py
+++ b/src/rrt_path_planner/python/src/star_wars_rrt.py
@@ -785,7 +785,9 @@ def _load_ship_models(self) -> dict[str, Any]:
try:
models["falcon"] = trimesh.load(model_path)
logging.info("Loaded ship model from %s", model_path)
- except Exception as exc: # noqa: BLE001 # pragma: no cover - visualization-only fallback
+ except (
+ Exception
+ ) as exc: # noqa: BLE001 # pragma: no cover - visualization-only fallback
logging.warning("Could not load STL model %s: %s", model_path, exc)
return models
diff --git a/src/shared/python/chat/_chat_dock_widget_qt.py b/src/shared/python/chat/_chat_dock_widget_qt.py
index 3794a1cf81..8f57dcd8b5 100644
--- a/src/shared/python/chat/_chat_dock_widget_qt.py
+++ b/src/shared/python/chat/_chat_dock_widget_qt.py
@@ -1091,12 +1091,12 @@ def switch_provider(
history_before = self._message_history
snapshot_before = list(history_before)
self._ai_settings_controller().switch_provider(name, model, thinking_level)
- assert self._message_history is history_before, (
- "switch_provider invariant: _message_history must remain the same list"
- )
- assert self._message_history == snapshot_before, (
- "switch_provider invariant: _message_history contents must not change"
- )
+ assert (
+ self._message_history is history_before
+ ), "switch_provider invariant: _message_history must remain the same list"
+ assert (
+ self._message_history == snapshot_before
+ ), "switch_provider invariant: _message_history contents must not change"
# ── Terminal mode ───────────────────────────────────────────────
diff --git a/src/shared/python/chat/_qt/ai_dropdowns.py b/src/shared/python/chat/_qt/ai_dropdowns.py
index 28b57c60f1..d6daf2c54c 100644
--- a/src/shared/python/chat/_qt/ai_dropdowns.py
+++ b/src/shared/python/chat/_qt/ai_dropdowns.py
@@ -255,9 +255,9 @@ def switch_provider(
history_before = dock._message_history
snapshot_before = list(history_before)
_controller_for(dock).switch_provider(name, model, thinking_level)
- assert dock._message_history is history_before, (
- "switch_provider invariant: _message_history must remain the same list"
- )
- assert dock._message_history == snapshot_before, (
- "switch_provider invariant: _message_history contents must not change"
- )
+ assert (
+ dock._message_history is history_before
+ ), "switch_provider invariant: _message_history must remain the same list"
+ assert (
+ dock._message_history == snapshot_before
+ ), "switch_provider invariant: _message_history contents must not change"
diff --git a/src/shared/python/chat/_qt/styling.py b/src/shared/python/chat/_qt/styling.py
index 5635556aa6..9dc0a39de7 100644
--- a/src/shared/python/chat/_qt/styling.py
+++ b/src/shared/python/chat/_qt/styling.py
@@ -23,6 +23,8 @@ def get_theme_colors(
try:
colors: dict[str, str] = provider.get_current_colors()
return colors
- except Exception: # noqa: BLE001 - defensive: a misbehaving provider must not crash the widget
+ except (
+ Exception
+ ): # noqa: BLE001 - defensive: a misbehaving provider must not crash the widget
colors = _DefaultDarkTheme().get_current_colors()
return colors
diff --git a/src/shared/python/chat/condensation/condenser.py b/src/shared/python/chat/condensation/condenser.py
index e02b4d553c..25b6e9135b 100644
--- a/src/shared/python/chat/condensation/condenser.py
+++ b/src/shared/python/chat/condensation/condenser.py
@@ -74,9 +74,9 @@ def condense(
preserved_anchors=_count_anchors(condensed),
)
- assert result.condensed_message_count >= 1, (
- "Condenser postcondition violated: must preserve at least one message"
- )
+ assert (
+ result.condensed_message_count >= 1
+ ), "Condenser postcondition violated: must preserve at least one message"
return result
def condense_to_session(
diff --git a/src/shared/python/humanoid_character_builder/core/model.py b/src/shared/python/humanoid_character_builder/core/model.py
index f4f8eeca21..df4925a0ac 100644
--- a/src/shared/python/humanoid_character_builder/core/model.py
+++ b/src/shared/python/humanoid_character_builder/core/model.py
@@ -97,7 +97,9 @@ def distance_to_edge(self, point: tuple[float, float]) -> float:
if point is None:
raise ValueError("point must be provided")
if not self.contains(point):
- return -1.0 # Or positive distance to polygon? Convention usually margin > 0 is stable.
+ return (
+ -1.0
+ ) # Or positive distance to polygon? Convention usually margin > 0 is stable.
# If outside, negative margin.
px, py = point
diff --git a/src/shared/python/model_generation/library/model_library.py b/src/shared/python/model_generation/library/model_library.py
index 6c82c309dc..45dcbd4a83 100644
--- a/src/shared/python/model_generation/library/model_library.py
+++ b/src/shared/python/model_generation/library/model_library.py
@@ -678,7 +678,9 @@ def _fetch_github_models(
)
continue
- with urllib.request.urlopen(subdir_url) as sub_response: # nosec B310
+ with urllib.request.urlopen(
+ subdir_url
+ ) as sub_response: # nosec B310
sub_contents = json.loads(sub_response.read().decode())
for sub_item in sub_contents:
if sub_item["type"] != "file":
diff --git a/src/shared/python/model_generation/tests/test_unified_loader.py b/src/shared/python/model_generation/tests/test_unified_loader.py
index 18e96a64d6..9245b3d968 100644
--- a/src/shared/python/model_generation/tests/test_unified_loader.py
+++ b/src/shared/python/model_generation/tests/test_unified_loader.py
@@ -789,8 +789,8 @@ def test_urdf_uses_bounded_precision(self) -> None:
stripped = part.lstrip("-").lstrip("0").replace(".", "")
stripped = stripped.lstrip("0")
# :.6g can produce up to 6 sig figs
- assert len(stripped) <= 6, (
- f"Value '{part}' has more than 6 significant digits"
- )
+ assert (
+ len(stripped) <= 6
+ ), f"Value '{part}' has more than 6 significant digits"
except ValueError:
pass # non-numeric attribute value
diff --git a/src/shared/python/plot_theme/tests/test_plot_theme.py b/src/shared/python/plot_theme/tests/test_plot_theme.py
index 56081a8f3e..870403e4ee 100644
--- a/src/shared/python/plot_theme/tests/test_plot_theme.py
+++ b/src/shared/python/plot_theme/tests/test_plot_theme.py
@@ -165,9 +165,9 @@ def test_all_themes_to_rcparams_succeeds(self):
for key, theme in PLOT_THEMES.items():
params = theme.to_rcparams()
- assert "figure.facecolor" in params, (
- f"Theme '{key}' missing figure.facecolor"
- )
+ assert (
+ "figure.facecolor" in params
+ ), f"Theme '{key}' missing figure.facecolor"
# ──────────────────────────────────────────────────────────────────────────────
diff --git a/src/shared/python/scripting/scripting_env.py b/src/shared/python/scripting/scripting_env.py
index 75750f9af9..12247d93db 100644
--- a/src/shared/python/scripting/scripting_env.py
+++ b/src/shared/python/scripting/scripting_env.py
@@ -560,7 +560,10 @@ def refresh_user_functions(self) -> None:
_screen_source_for_escapes(code)
# Execute within current namespace so imports/functions are persistent
exec(code, self.namespace) # nosec B102
- except (SecurityError, *USER_CODE_ERROR_TYPES) as e: # noqa: BLE001 — user library code may raise anything; report and continue
+ except (
+ SecurityError,
+ *USER_CODE_ERROR_TYPES,
+ ) as e: # noqa: BLE001 — user library code may raise anything; report and continue
sys.stderr.write(f"Error loading user library: {e}\n")
sys.stderr.flush()
diff --git a/src/shared/python/sidekick/calculators/mechanical/trc_geometry.py b/src/shared/python/sidekick/calculators/mechanical/trc_geometry.py
index c5bc513428..5c9a6182a1 100644
--- a/src/shared/python/sidekick/calculators/mechanical/trc_geometry.py
+++ b/src/shared/python/sidekick/calculators/mechanical/trc_geometry.py
@@ -316,12 +316,12 @@ def calculate_geometry(
VesselGeometryResult containing detailed calculations
"""
# DbC preconditions
- assert dimensions.cylinder_diameter > 0, (
- f"cylinder_diameter must be positive, got {dimensions.cylinder_diameter}"
- )
- assert dimensions.cylinder_height > 0, (
- f"cylinder_height must be positive, got {dimensions.cylinder_height}"
- )
+ assert (
+ dimensions.cylinder_diameter > 0
+ ), f"cylinder_diameter must be positive, got {dimensions.cylinder_diameter}"
+ assert (
+ dimensions.cylinder_height > 0
+ ), f"cylinder_height must be positive, got {dimensions.cylinder_height}"
results = VesselGeometryResult()
if not layers:
diff --git a/src/shared/python/sidekick/process_calculators/psa_package/psa_gui.py b/src/shared/python/sidekick/process_calculators/psa_package/psa_gui.py
index a3b2043dd8..b19445472d 100644
--- a/src/shared/python/sidekick/process_calculators/psa_package/psa_gui.py
+++ b/src/shared/python/sidekick/process_calculators/psa_package/psa_gui.py
@@ -119,7 +119,9 @@ def __init__(
self.fig = Figure(figsize=(width, height), dpi=100) # noqa: F821
super().__init__(self.fig)
self.setParent(parent)
- self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # noqa: F821
+ self.setSizePolicy(
+ QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
+ ) # noqa: F821
class InputPanel(QWidget): # noqa: F811, F821
@@ -174,7 +176,9 @@ def _setup_ui(self) -> None:
layout.addWidget(op_group)
# Component Data Group
- comp_group = QGroupBox("Component Data (Feed % | S1 Removal % | S2 Removal %)") # noqa: F821
+ comp_group = QGroupBox(
+ "Component Data (Feed % | S1 Removal % | S2 Removal %)"
+ ) # noqa: F821
comp_layout = QVBoxLayout() # noqa: F821
self.component_table = QTableWidget(7, 4) # noqa: F821
@@ -186,8 +190,12 @@ def _setup_ui(self) -> None:
header.setVisible(False)
for i, comp in enumerate(DEFAULT_COMPONENTS): # noqa: F821
- self.component_table.setItem(i, 0, QTableWidgetItem(comp["name"])) # noqa: F821
- self.component_table.setItem(i, 1, QTableWidgetItem(str(comp["feed_pct"]))) # noqa: F821
+ self.component_table.setItem(
+ i, 0, QTableWidgetItem(comp["name"])
+ ) # noqa: F821
+ self.component_table.setItem(
+ i, 1, QTableWidgetItem(str(comp["feed_pct"]))
+ ) # noqa: F821
self.component_table.setItem(
i,
2,
@@ -229,7 +237,9 @@ def _reset_defaults(self) -> None:
self.prod_recycle_slider.setValue(0)
for i, comp in enumerate(DEFAULT_COMPONENTS): # noqa: F821
- self.component_table.setItem(i, 1, QTableWidgetItem(str(comp["feed_pct"]))) # noqa: F821
+ self.component_table.setItem(
+ i, 1, QTableWidgetItem(str(comp["feed_pct"]))
+ ) # noqa: F821
self.component_table.setItem(
i,
2,
@@ -408,7 +418,9 @@ def _update_safety_metrics(self, results: PSAResults) -> None: # noqa: F821
self.s2_tail_h2_label.setText(f"{results.s2_tail_h2_pct:.2f}%")
self.s2_tail_o2_label.setText(f"{results.s2_tail_o2_pct:.2f}%")
- status = get_flammability_status(results.s2_tail_h2_pct, results.s2_tail_o2_pct) # noqa: F821
+ status = get_flammability_status(
+ results.s2_tail_h2_pct, results.s2_tail_o2_pct
+ ) # noqa: F821
self.flammability_label.setText(status)
if "CRITICAL" in status or "FLAMMABLE" in status or "DANGEROUS" in status:
@@ -680,7 +692,9 @@ def _plot_o2_safety(self) -> None:
"""Plot O2 safety analysis."""
num_points = min(self.num_points_spin.value(), 51) # Cap at 51 for O2 analysis
inlet_o2_values = np.array([0.5, 1.0, 2.0, 5.0], dtype=np.float64) # noqa: F821
- s1_removal_range = np.linspace(50.0, 95.0, num_points, dtype=np.float64) # noqa: F821
+ s1_removal_range = np.linspace(
+ 50.0, 95.0, num_points, dtype=np.float64
+ ) # noqa: F821
o2_analysis = calculate_o2_safety_analysis( # noqa: F821
inlet_o2_pcts=inlet_o2_values,
@@ -949,7 +963,8 @@ def _launch_colab(self) -> None:
"3. Copy the notebook content manually"
)
msg.setStandardButtons(
- QMessageBox.StandardButton.Open | QMessageBox.StandardButton.Cancel # noqa: F821
+ QMessageBox.StandardButton.Open
+ | QMessageBox.StandardButton.Cancel # noqa: F821
)
msg.setDefaultButton(QMessageBox.StandardButton.Open) # noqa: F821
@@ -1079,7 +1094,9 @@ def _calculate(self) -> None:
self.sensitivity_widget.set_components(components)
except ValueError as e:
- QMessageBox.warning(self, "Input Error", f"Invalid input: {e}") # noqa: F821
+ QMessageBox.warning(
+ self, "Input Error", f"Invalid input: {e}"
+ ) # noqa: F821
except (RuntimeError, AttributeError) as e:
QMessageBox.critical(self, "Calculation Error", f"Error: {e}") # noqa: F821
diff --git a/src/shared/python/sidekick/standalone/preferences.py b/src/shared/python/sidekick/standalone/preferences.py
index 1ee43ecc31..b791e9324f 100644
--- a/src/shared/python/sidekick/standalone/preferences.py
+++ b/src/shared/python/sidekick/standalone/preferences.py
@@ -79,9 +79,9 @@ class StandalonePreferences:
def __init__(self, store: Any = None) -> None:
if store is None:
store = _default_store()
- assert hasattr(store, "get") and hasattr(store, "set"), (
- "store must implement get() and set()"
- )
+ assert hasattr(store, "get") and hasattr(
+ store, "set"
+ ), "store must implement get() and set()"
self._store = store
# ------------------------------------------------------------------
@@ -184,9 +184,9 @@ def apply_tokens(self, theme_colors: dict[str, str]) -> dict[str, str]:
Postcondition: every key in ``COLOR_TOKEN_MAP`` that maps to a key
present in ``theme_colors`` appears in the result.
"""
- assert isinstance(theme_colors, dict) and theme_colors, (
- "theme_colors must be a non-empty dict"
- )
+ assert (
+ isinstance(theme_colors, dict) and theme_colors
+ ), "theme_colors must be a non-empty dict"
from theme.sidekick_tokens import COLOR_TOKEN_MAP, DEFAULT_SIDEKICK_TOKENS
tokens: dict[str, str] = dict(DEFAULT_SIDEKICK_TOKENS)
@@ -194,9 +194,9 @@ def apply_tokens(self, theme_colors: dict[str, str]) -> dict[str, str]:
if theme_key in theme_colors:
tokens[token_name] = theme_colors[theme_key]
- assert all(isinstance(v, str) for v in tokens.values()), (
- "postcondition: all token values must be strings"
- )
+ assert all(
+ isinstance(v, str) for v in tokens.values()
+ ), "postcondition: all token values must be strings"
return tokens
diff --git a/src/shared/python/sidekick/standalone/runner.py b/src/shared/python/sidekick/standalone/runner.py
index fd7bc80254..7e672fbeb0 100644
--- a/src/shared/python/sidekick/standalone/runner.py
+++ b/src/shared/python/sidekick/standalone/runner.py
@@ -246,9 +246,9 @@ def run_calculator(
*calculator* must be a non-empty string.
*inputs_path* must point to a readable JSON file.
"""
- assert isinstance(calculator, str) and calculator, (
- "calculator name must be non-empty"
- )
+ assert (
+ isinstance(calculator, str) and calculator
+ ), "calculator name must be non-empty"
assert isinstance(inputs_path, str) and inputs_path, "inputs_path must be non-empty"
_ensure_registered()
diff --git a/src/shared/python/sidekick/tests/process_calculators/test_psa_model.py b/src/shared/python/sidekick/tests/process_calculators/test_psa_model.py
index 839da96d03..2cc8b507d9 100644
--- a/src/shared/python/sidekick/tests/process_calculators/test_psa_model.py
+++ b/src/shared/python/sidekick/tests/process_calculators/test_psa_model.py
@@ -107,9 +107,9 @@ def test_s2_tail_vent_flow(self, base_results) -> None:
def test_mass_balance(self, base_results) -> None:
"""Test mass balance closure."""
- assert abs(base_results.mass_balance_error) < 1e-10, (
- f"Mass balance error too large: {base_results.mass_balance_error}"
- )
+ assert (
+ abs(base_results.mass_balance_error) < 1e-10
+ ), f"Mass balance error too large: {base_results.mass_balance_error}"
def test_s2_tail_h2_pct(self, base_results) -> None:
"""Test S2 tail H2 percentage matches Excel."""
@@ -463,9 +463,9 @@ def test_flow_conservation_per_component(self) -> None:
- results.flows.s2_tail_vent[i]
- results.flows.net_product[i]
)
- assert abs(balance) < 1e-10, (
- f"Mass balance error for {results.component_names[i]}: {balance}"
- )
+ assert (
+ abs(balance) < 1e-10
+ ), f"Mass balance error for {results.component_names[i]}: {balance}"
def test_mixed_feed_balance(self) -> None:
"""Test mixed feed balance."""
diff --git a/src/shared/python/sidekick/tests/process_calculators/test_syngas_compression_dedup.py b/src/shared/python/sidekick/tests/process_calculators/test_syngas_compression_dedup.py
index 718c929928..2542d1a77d 100644
--- a/src/shared/python/sidekick/tests/process_calculators/test_syngas_compression_dedup.py
+++ b/src/shared/python/sidekick/tests/process_calculators/test_syngas_compression_dedup.py
@@ -41,9 +41,9 @@ def test_single_syngas_compression_engine_definition() -> None:
def test_dead_syngas_compression_subpackage_removed() -> None:
"""The empty placeholder ``syngas_compression/`` subpackage is gone."""
dead_dir = _PROCESS_CALCULATORS / "syngas_compression"
- assert not dead_dir.exists(), (
- "Dead placeholder subpackage should have been deleted (#3183)"
- )
+ assert (
+ not dead_dir.exists()
+ ), "Dead placeholder subpackage should have been deleted (#3183)"
def test_root_calculator_exposes_real_engine() -> None:
diff --git a/src/shared/python/sidekick/tests/test_json_io_boundary_3333.py b/src/shared/python/sidekick/tests/test_json_io_boundary_3333.py
index d51377e34c..c2a45172d6 100644
--- a/src/shared/python/sidekick/tests/test_json_io_boundary_3333.py
+++ b/src/shared/python/sidekick/tests/test_json_io_boundary_3333.py
@@ -32,9 +32,9 @@ def test_state_manager_has_no_cross_tree_imports() -> None:
root = node.module.split(".")[0]
if root in {"utils", "compatibility"}:
offending.append(node.module)
- assert offending == [], (
- f"state_manager still imports across the tool-tree boundary: {offending}"
- )
+ assert (
+ offending == []
+ ), f"state_manager still imports across the tool-tree boundary: {offending}"
@pytest.mark.unit
diff --git a/src/shared/python/sidekick/ui/tools_sidebar/registry.py b/src/shared/python/sidekick/ui/tools_sidebar/registry.py
index 70ffb40077..679264717e 100644
--- a/src/shared/python/sidekick/ui/tools_sidebar/registry.py
+++ b/src/shared/python/sidekick/ui/tools_sidebar/registry.py
@@ -212,7 +212,9 @@ def _notify(self, event: WorkspaceEvent, name: str) -> None:
continue
try:
subscription.callback(queued_event, queued_name)
- except Exception: # noqa: BLE001 - subscribers must not break notify
+ except (
+ Exception
+ ): # noqa: BLE001 - subscribers must not break notify
_logger.exception(
"Workspace subscriber raised on %s '%s'",
queued_event,
diff --git a/src/shared/python/sidekick/ui/tools_sidebar/sidebar.py b/src/shared/python/sidekick/ui/tools_sidebar/sidebar.py
index a7bdb28a35..c58d7ad1f7 100644
--- a/src/shared/python/sidekick/ui/tools_sidebar/sidebar.py
+++ b/src/shared/python/sidekick/ui/tools_sidebar/sidebar.py
@@ -769,7 +769,9 @@ def _persist_visible_tabs(self) -> None:
self._vis_persistence.save(self._tab_collection.visible_ids())
def _apply_tab_state(self, state: SidebarState) -> None:
- self._state = sanitize_tab_state(state, self._tab_collection._tab_definitions) # noqa: SLF001
+ self._state = sanitize_tab_state(
+ state, self._tab_collection._tab_definitions
+ ) # noqa: SLF001
state = self._state
for tab_id in list(self._tab_collection.visible_ids()):
if tab_id in state.hidden_tabs:
diff --git a/src/shared/python/tests/test_god_class_guard.py b/src/shared/python/tests/test_god_class_guard.py
index b6f6c4f050..a15f3518d5 100644
--- a/src/shared/python/tests/test_god_class_guard.py
+++ b/src/shared/python/tests/test_god_class_guard.py
@@ -124,9 +124,10 @@ def test_no_god_classes_in_monitored_files() -> None:
"Refactor or add to KNOWN_CLASSES with justification (GH1692)."
)
- assert not violations, (
- "God class ceiling exceeded in monitored files:\n"
- + "\n".join(f" - {v}" for v in violations)
+ assert (
+ not violations
+ ), "God class ceiling exceeded in monitored files:\n" + "\n".join(
+ f" - {v}" for v in violations
)
@@ -150,9 +151,9 @@ def test_calculator_state_mixin_reduced() -> None:
)
# Also verify sub-mixins exist and are bounded
- assert "_SplitterStateMixin" in counts, (
- "_SplitterStateMixin sub-mixin missing from calculator_state_mixin.py"
- )
- assert "_ClipboardMixin" in counts, (
- "_ClipboardMixin sub-mixin missing from calculator_state_mixin.py"
- )
+ assert (
+ "_SplitterStateMixin" in counts
+ ), "_SplitterStateMixin sub-mixin missing from calculator_state_mixin.py"
+ assert (
+ "_ClipboardMixin" in counts
+ ), "_ClipboardMixin sub-mixin missing from calculator_state_mixin.py"
diff --git a/src/shared/python/theme/zoom.py b/src/shared/python/theme/zoom.py
index 7b12cf40d8..e50b0a36bc 100644
--- a/src/shared/python/theme/zoom.py
+++ b/src/shared/python/theme/zoom.py
@@ -142,7 +142,9 @@ def reset_zoom(self) -> None:
"""Reset application zoom to the configured default."""
self.set_zoom_percent(self._config.default_percent)
- def eventFilter(self, obj: QObject | None, event: QEvent | None) -> bool: # noqa: N802
+ def eventFilter(
+ self, obj: QObject | None, event: QEvent | None
+ ) -> bool: # noqa: N802
"""Handle Ctrl+wheel and Ctrl+shortcut app zoom events."""
if event is None:
return False
diff --git a/src/web_applications/urdf_viewer/tests/test_urdf_viewer.py b/src/web_applications/urdf_viewer/tests/test_urdf_viewer.py
index a65550442e..83acebd114 100644
--- a/src/web_applications/urdf_viewer/tests/test_urdf_viewer.py
+++ b/src/web_applications/urdf_viewer/tests/test_urdf_viewer.py
@@ -15,7 +15,9 @@
# (CI may lack python-multipart, cors deps, etc.)
try:
from app import app
-except Exception as _exc: # noqa: BLE001 — CI may lack optional deps; skip entire module
+except (
+ Exception
+) as _exc: # noqa: BLE001 — CI may lack optional deps; skip entire module
pytest.skip(
f"Skipping urdf_viewer tests — app import failed: {_exc}",
allow_module_level=True,
diff --git a/tests/architecture/test_gh1696_god_modules.py b/tests/architecture/test_gh1696_god_modules.py
index 90155f871b..f2fb7c223c 100644
--- a/tests/architecture/test_gh1696_god_modules.py
+++ b/tests/architecture/test_gh1696_god_modules.py
@@ -241,15 +241,15 @@ def test_signal_toolkit_uses_lazy_import_pattern() -> None:
"signal_toolkit must contain a LAZY dispatch table in __init__.py "
"or _lazy_map.py"
)
- assert SIGNAL_TOOLKIT_LAZY_MAP.exists(), (
- "_lazy_map.py must exist alongside __init__.py (issue #1696 refactor)"
- )
- assert "def __getattr__" in init_source, (
- "signal_toolkit/__init__.py must define __getattr__ for lazy loading"
- )
- assert "importlib.import_module" in init_source, (
- "signal_toolkit/__init__.py must use importlib.import_module in __getattr__"
- )
+ assert (
+ SIGNAL_TOOLKIT_LAZY_MAP.exists()
+ ), "_lazy_map.py must exist alongside __init__.py (issue #1696 refactor)"
+ assert (
+ "def __getattr__" in init_source
+ ), "signal_toolkit/__init__.py must define __getattr__ for lazy loading"
+ assert (
+ "importlib.import_module" in init_source
+ ), "signal_toolkit/__init__.py must use importlib.import_module in __getattr__"
@pytest.mark.unit
@@ -270,9 +270,9 @@ def test_signal_toolkit_lazy_attribute_loads_on_access() -> None:
assert obj is not None, "signal_toolkit.SeriesExpansion should not be None"
# After access, should be cached in globals
- assert "SeriesExpansion" in signal_toolkit.__dict__, (
- "After access, SeriesExpansion must be cached in signal_toolkit.__dict__"
- )
+ assert (
+ "SeriesExpansion" in signal_toolkit.__dict__
+ ), "After access, SeriesExpansion must be cached in signal_toolkit.__dict__"
@pytest.mark.unit
@@ -284,9 +284,9 @@ def test_signal_toolkit_all_exports_accessible() -> None:
attr = getattr(signal_toolkit, name, None)
# HAS_* flags and optional widgets may be None (no PyQt6 in CI)
if name not in {"PolynomialGeneratorWidget", "SignalToolkitWidget"}:
- assert attr is not None, (
- f"signal_toolkit.{name} is None — lazy import may be broken"
- )
+ assert (
+ attr is not None
+ ), f"signal_toolkit.{name} is None — lazy import may be broken"
@pytest.mark.unit
diff --git a/tests/architecture/test_sidekick_external_imports_3316.py b/tests/architecture/test_sidekick_external_imports_3316.py
index 9bc0009438..b28742fe66 100644
--- a/tests/architecture/test_sidekick_external_imports_3316.py
+++ b/tests/architecture/test_sidekick_external_imports_3316.py
@@ -96,8 +96,7 @@ def test_legacy_sidekick_aliases_share_canonical_module_objects() -> None:
"-W",
"ignore::DeprecationWarning",
"-c",
- textwrap.dedent(
- """
+ textwrap.dedent("""
import importlib
canonical = importlib.import_module(
@@ -114,8 +113,7 @@ def test_legacy_sidekick_aliases_share_canonical_module_objects() -> None:
"src.shared.python.sidekick.ui.tools_sidebar.registry"
)
assert src_alias is None or src_alias is canonical
- """
- ),
+ """),
],
cwd=REPO_ROOT,
env=env,
diff --git a/tests/calc_backend/test_wgs_reactor_headless_import_3317.py b/tests/calc_backend/test_wgs_reactor_headless_import_3317.py
index fc51b50c2a..203c0eb5b9 100644
--- a/tests/calc_backend/test_wgs_reactor_headless_import_3317.py
+++ b/tests/calc_backend/test_wgs_reactor_headless_import_3317.py
@@ -22,8 +22,7 @@
# Program run in a clean subprocess: block PyQt6 (and the theme layer that wraps
# it), then import the engine and assert success + no Qt/theme leakage.
-_PROGRAM = textwrap.dedent(
- """
+_PROGRAM = textwrap.dedent("""
import importlib.abc
import importlib.machinery
import sys
@@ -57,8 +56,7 @@ def find_spec(self, fullname, path, target=None):
assert "PyQt6" not in sys.modules
print("HEADLESS_OK")
- """
-)
+ """)
@pytest.mark.unit
diff --git a/tests/conftest.py b/tests/conftest.py
index ee83891375..e8ef7cfe07 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -131,6 +131,7 @@ def qapp():
if app is None:
app = QApplication(sys.argv)
yield app
+
except ImportError:
pass
diff --git a/tests/data_processing/data_processor/test_script_generator_hardening.py b/tests/data_processing/data_processor/test_script_generator_hardening.py
index 101e6bae2a..14c6f33d49 100644
--- a/tests/data_processing/data_processor/test_script_generator_hardening.py
+++ b/tests/data_processing/data_processor/test_script_generator_hardening.py
@@ -91,9 +91,9 @@ def test_batch_script_path_metacharacters_are_safely_serialized() -> None:
and node.targets[0].id == "input_patterns"
):
value = ast.literal_eval(node.value)
- assert value == [dangerous_path], (
- f"Path did not round-trip safely: got {value!r}"
- )
+ assert value == [
+ dangerous_path
+ ], f"Path did not round-trip safely: got {value!r}"
found = True
break
assert found, "input_patterns assignment not found in generated script"
diff --git a/tests/heavy_integration/test_tools_contracts.py b/tests/heavy_integration/test_tools_contracts.py
index f809601d08..e2606e3e09 100644
--- a/tests/heavy_integration/test_tools_contracts.py
+++ b/tests/heavy_integration/test_tools_contracts.py
@@ -30,9 +30,9 @@ def test_box_mesh_is_watertight_and_valid_volume(self) -> None:
box = trimesh.creation.box((1.0, 2.0, 3.0))
assert box.is_watertight, "Box mesh must be watertight for URDF/physics use"
- assert box.volume == pytest.approx(6.0, rel=1e-4), (
- f"Expected volume 6.0, got {box.volume}"
- )
+ assert box.volume == pytest.approx(
+ 6.0, rel=1e-4
+ ), f"Expected volume 6.0, got {box.volume}"
assert len(box.vertices) > 0
assert len(box.faces) > 0
@@ -75,16 +75,16 @@ def test_butterworth_filter_attenuation(self) -> None:
freq = w / (2 * np.pi)
# Passband gain at DC should be ~1.0
dc_gain = abs(h[0])
- assert dc_gain == pytest.approx(1.0, abs=0.01), (
- f"DC gain should be 1.0, got {dc_gain}"
- )
+ assert dc_gain == pytest.approx(
+ 1.0, abs=0.01
+ ), f"DC gain should be 1.0, got {dc_gain}"
# Stopband attenuation at 0.5 should be < -20 dB
stop_idx = int(0.5 * len(freq))
stop_gain_db = 20 * np.log10(abs(h[stop_idx]) + 1e-12)
- assert stop_gain_db < -20, (
- f"Expected > 20 dB attenuation, got {stop_gain_db:.1f} dB"
- )
+ assert (
+ stop_gain_db < -20
+ ), f"Expected > 20 dB attenuation, got {stop_gain_db:.1f} dB"
def test_fft_roundtrip(self) -> None:
"""FFT→IFFT roundtrip preserves signal — fundamental DSP contract."""
diff --git a/tests/integration/test_cross_repo_contracts.py b/tests/integration/test_cross_repo_contracts.py
index f2c503e518..dadb4aa6c4 100644
--- a/tests/integration/test_cross_repo_contracts.py
+++ b/tests/integration/test_cross_repo_contracts.py
@@ -127,13 +127,13 @@ def test_validate_composition_signature(self) -> None:
sig = inspect.signature(InputValidator.validate_composition)
params = sig.parameters
- assert "composition" in params, (
- "validate_composition must have 'composition' param"
- )
+ assert (
+ "composition" in params
+ ), "validate_composition must have 'composition' param"
assert "tolerance" in params, "validate_composition must have 'tolerance' param"
- assert params["tolerance"].default is not inspect.Parameter.empty, (
- "validate_composition 'tolerance' must have a default value"
- )
+ assert (
+ params["tolerance"].default is not inspect.Parameter.empty
+ ), "validate_composition 'tolerance' must have a default value"
# ---------------------------------------------------------------------------
@@ -271,18 +271,18 @@ def test_baghouse_calculator_has_calculate_method(self) -> None:
"""BaghouseCalculator must expose a 'calculate' method."""
from upstream_drift_tools.process_calculators import BaghouseCalculator
- assert hasattr(BaghouseCalculator, "calculate"), (
- "BaghouseCalculator.calculate() is missing"
- )
+ assert hasattr(
+ BaghouseCalculator, "calculate"
+ ), "BaghouseCalculator.calculate() is missing"
assert callable(BaghouseCalculator.calculate)
def test_financial_calculator_has_calculate_method(self) -> None:
"""FinancialCalculator must expose a 'calculate' method."""
from upstream_drift_tools.process_calculators import FinancialCalculator
- assert hasattr(FinancialCalculator, "calculate"), (
- "FinancialCalculator.calculate() is missing"
- )
+ assert hasattr(
+ FinancialCalculator, "calculate"
+ ), "FinancialCalculator.calculate() is missing"
assert callable(FinancialCalculator.calculate)
def test_flare_design_is_dataclass_or_namedtuple(self) -> None:
@@ -377,9 +377,9 @@ def test_all_exceptions_are_exception_subclasses(self) -> None:
FitError,
UnsupportedOperationError,
):
- assert issubclass(exc_cls, Exception), (
- f"{exc_cls.__name__} must be a subclass of Exception"
- )
+ assert issubclass(
+ exc_cls, Exception
+ ), f"{exc_cls.__name__} must be a subclass of Exception"
def test_specific_exceptions_are_subclass_of_base(self) -> None:
"""Specific exceptions must be catchable via the base DataProcessingError.
@@ -466,9 +466,9 @@ def test_symbol_importable_from_contracts(self, symbol: str) -> None:
import importlib
mod = importlib.import_module("contracts")
- assert hasattr(mod, symbol), (
- f"contracts.{symbol} is missing — downstream repos import it by name"
- )
+ assert hasattr(
+ mod, symbol
+ ), f"contracts.{symbol} is missing — downstream repos import it by name"
def test_require_is_callable(self) -> None:
"""require must be callable (function or callable class)."""
@@ -486,14 +486,14 @@ def test_contract_level_has_off_variant(self) -> None:
"""ContractLevel must have an OFF member — used to disable checks in prod."""
from contracts import ContractLevel
- assert hasattr(ContractLevel, "OFF"), (
- "ContractLevel.OFF is required — downstream repos set it in production"
- )
+ assert hasattr(
+ ContractLevel, "OFF"
+ ), "ContractLevel.OFF is required — downstream repos set it in production"
def test_contract_level_has_enforce_variant(self) -> None:
"""ContractLevel must have an ENFORCE member — used in test environments."""
from contracts import ContractLevel
- assert hasattr(ContractLevel, "ENFORCE"), (
- "ContractLevel.ENFORCE is required — downstream test suites activate it"
- )
+ assert hasattr(
+ ContractLevel, "ENFORCE"
+ ), "ContractLevel.ENFORCE is required — downstream test suites activate it"
diff --git a/tests/ode_solver/test_ode_solver_timeout.py b/tests/ode_solver/test_ode_solver_timeout.py
index c6dcafb884..6af1081763 100644
--- a/tests/ode_solver/test_ode_solver_timeout.py
+++ b/tests/ode_solver/test_ode_solver_timeout.py
@@ -162,9 +162,9 @@ def test_exponential_decay_completes_in_budget(self, ode_solver: type) -> None:
elapsed = time.perf_counter() - start
assert sol is not None
- assert elapsed < 5.0, (
- f"Exponential decay solve took {elapsed:.3f} s, expected < 5 s"
- )
+ assert (
+ elapsed < 5.0
+ ), f"Exponential decay solve took {elapsed:.3f} s, expected < 5 s"
def test_harmonic_oscillator_completes_in_budget(self, ode_solver: type) -> None:
"""Harmonic oscillator solve completes in < 5 s (typical: < 0.1 s).
@@ -182,9 +182,9 @@ def test_harmonic_oscillator_completes_in_budget(self, ode_solver: type) -> None
elapsed = time.perf_counter() - start
assert sol is not None
- assert elapsed < 5.0, (
- f"Harmonic oscillator solve took {elapsed:.3f} s, expected < 5 s"
- )
+ assert (
+ elapsed < 5.0
+ ), f"Harmonic oscillator solve took {elapsed:.3f} s, expected < 5 s"
def test_lotka_volterra_completes_in_budget(self, ode_solver: type) -> None:
"""Lotka-Volterra (predator-prey) solve completes in < 5 s.
@@ -205,9 +205,9 @@ def test_lotka_volterra_completes_in_budget(self, ode_solver: type) -> None:
elapsed = time.perf_counter() - start
assert sol is not None
- assert elapsed < 5.0, (
- f"Lotka-Volterra solve took {elapsed:.3f} s, expected < 5 s"
- )
+ assert (
+ elapsed < 5.0
+ ), f"Lotka-Volterra solve took {elapsed:.3f} s, expected < 5 s"
def test_with_timeout_overhead_is_negligible(self) -> None:
"""with_timeout wrapper adds < 100 ms overhead for fast operations.
@@ -226,6 +226,6 @@ def trivial() -> int:
assert result == 42
avg_ms = (elapsed / 100) * 1000
- assert avg_ms < 100, (
- f"with_timeout average overhead {avg_ms:.1f} ms/call, expected < 100 ms"
- )
+ assert (
+ avg_ms < 100
+ ), f"with_timeout average overhead {avg_ms:.1f} ms/call, expected < 100 ms"
diff --git a/tests/ops/test_detect_secrets_baseline.py b/tests/ops/test_detect_secrets_baseline.py
index 97825ff5d7..2df7ec0e4a 100644
--- a/tests/ops/test_detect_secrets_baseline.py
+++ b/tests/ops/test_detect_secrets_baseline.py
@@ -141,9 +141,9 @@ def test_workflow_invokes_installed_python_module(self) -> None:
def test_baseline_file_exists(self) -> None:
"""Precondition: .secrets.baseline must exist in repo root."""
- assert BASELINE_PATH.exists(), (
- ".secrets.baseline is missing. Run: detect-secrets scan > .secrets.baseline"
- )
+ assert (
+ BASELINE_PATH.exists()
+ ), ".secrets.baseline is missing. Run: detect-secrets scan > .secrets.baseline"
def test_baseline_is_valid_json(self) -> None:
"""Baseline must be parseable JSON."""
@@ -352,9 +352,9 @@ def test_all_entries_have_required_field(self, required_field: str) -> None:
for i, entry in enumerate(entries):
if required_field not in entry:
missing.append(f"{file_key}[{i}]")
- assert not missing, (
- f"Baseline entries missing field {required_field!r}: {missing[:10]}"
- )
+ assert (
+ not missing
+ ), f"Baseline entries missing field {required_field!r}: {missing[:10]}"
def test_hashed_secrets_are_40_char_hex(self) -> None:
"""All hashed_secret values must be 40-char hex strings (SHA1)."""
diff --git a/tests/p1am_control_system/test_backend_security.py b/tests/p1am_control_system/test_backend_security.py
index 52f639a0da..dba5e33191 100644
--- a/tests/p1am_control_system/test_backend_security.py
+++ b/tests/p1am_control_system/test_backend_security.py
@@ -268,9 +268,9 @@ def test_import_failure_leaves_db_intact(monkeypatch: pytest.MonkeyPatch) -> Non
with Session(_test_engine) as s:
tags = s.exec(select(TagDefinitionDb)).all()
- assert any(t.name == "EXISTING_TAG" for t in tags), (
- "import failure wiped the existing plant DB"
- )
+ assert any(
+ t.name == "EXISTING_TAG" for t in tags
+ ), "import failure wiped the existing plant DB"
def test_safe_extract_rejects_path_traversal(tmp_path) -> None:
diff --git a/tests/p1am_control_system/test_backend_security_import_guard.py b/tests/p1am_control_system/test_backend_security_import_guard.py
index e2cec7adfa..7f81a19ea0 100644
--- a/tests/p1am_control_system/test_backend_security_import_guard.py
+++ b/tests/p1am_control_system/test_backend_security_import_guard.py
@@ -40,9 +40,9 @@ def test_backend_import_guard_only_catches_module_not_found() -> None:
"bare 'except:' would swallow real backend defects and skip the "
"security suite"
)
- assert isinstance(exc_type, ast.Name), (
- "import guard must catch a single named exception, not a tuple/attr"
- )
+ assert isinstance(
+ exc_type, ast.Name
+ ), "import guard must catch a single named exception, not a tuple/attr"
assert exc_type.id == "ModuleNotFoundError", (
"backend import guard must narrow to ModuleNotFoundError so that a "
"NameError/SyntaxError/ImportError in the backend fails loudly "
diff --git a/tests/p1am_control_system/test_event_logger_filter_error_logging.py b/tests/p1am_control_system/test_event_logger_filter_error_logging.py
index 2008094278..ef1391b94d 100644
--- a/tests/p1am_control_system/test_event_logger_filter_error_logging.py
+++ b/tests/p1am_control_system/test_event_logger_filter_error_logging.py
@@ -50,8 +50,8 @@ def _boom() -> list[str]:
with caplog.at_level(logging.ERROR, logger=event_logger.__name__):
event_logger.EventLogViewerWidget.update_event_types_combobox(widget)
- assert any("event-type filter" in rec.getMessage() for rec in caplog.records), (
- "DB failure must be logged"
- )
+ assert any(
+ "event-type filter" in rec.getMessage() for rec in caplog.records
+ ), "DB failure must be logged"
# Combobox still has the default 'All' entry and did not raise.
assert widget.event_type_combo._items == ["All"]
diff --git a/tests/programmatic_pid/test_equipment.py b/tests/programmatic_pid/test_equipment.py
index f286c32eb2..aa31a25476 100644
--- a/tests/programmatic_pid/test_equipment.py
+++ b/tests/programmatic_pid/test_equipment.py
@@ -108,9 +108,9 @@ def test_draw_equipment_symbol_uses_registry():
for eq_type in ["hopper", "fan", "gate_valve", "control_valve", "pump"]:
initial_count = len(list(msp))
draw_equipment_symbol(msp, _eq(etype=eq_type), "EQUIPMENT")
- assert len(list(msp)) > initial_count, (
- f"{eq_type} should add entities to modelspace"
- )
+ assert (
+ len(list(msp)) > initial_count
+ ), f"{eq_type} should add entities to modelspace"
def test_draw_equipment_symbol_fallback_to_box():
diff --git a/tests/programmatic_pid/test_profiles_extra.py b/tests/programmatic_pid/test_profiles_extra.py
index 514a26fb5a..ab9a2a8b6c 100644
--- a/tests/programmatic_pid/test_profiles_extra.py
+++ b/tests/programmatic_pid/test_profiles_extra.py
@@ -190,9 +190,9 @@ def test_all_presets_share_same_layout_keys(self) -> None:
keysets = [
set(p["layout"].keys()) for p in PROFILE_PRESETS.values() if "layout" in p
]
- assert all(k == keysets[0] for k in keysets), (
- "all presets must declare the same layout keys for predictable merging"
- )
+ assert all(
+ k == keysets[0] for k in keysets
+ ), "all presets must declare the same layout keys for predictable merging"
def test_presentation_has_no_defaults_section(self) -> None:
# presentation preset deliberately omits a defaults block.
diff --git a/tests/project_packer_fixes/test_build_exe_lod.py b/tests/project_packer_fixes/test_build_exe_lod.py
index 570ebb996b..27501ab365 100644
--- a/tests/project_packer_fixes/test_build_exe_lod.py
+++ b/tests/project_packer_fixes/test_build_exe_lod.py
@@ -39,9 +39,9 @@ def test_check_pyinstaller_uses_find_spec_directly(self, build_exe_module) -> No
import inspect
source = inspect.getsource(build_exe_module.check_pyinstaller)
- assert "importlib.util.find_spec" not in source, (
- "LoD violation: should use find_spec directly, not importlib.util.find_spec"
- )
+ assert (
+ "importlib.util.find_spec" not in source
+ ), "LoD violation: should use find_spec directly, not importlib.util.find_spec"
assert "find_spec" in source, "check_pyinstaller should call find_spec"
def test_check_pyinstaller_available(self, build_exe_module) -> None:
@@ -60,9 +60,9 @@ def test_check_pyinstaller_not_available(self, build_exe_module) -> None:
def test_find_spec_import_at_module_level(self, build_exe_module) -> None:
"""Verify find_spec is imported at module level (not accessed via importlib.util)."""
- assert hasattr(build_exe_module, "find_spec"), (
- "find_spec must be imported at module level in build_exe"
- )
+ assert hasattr(
+ build_exe_module, "find_spec"
+ ), "find_spec must be imported at module level in build_exe"
def test_install_pyinstaller_success(self, build_exe_module) -> None:
"""Test successful PyInstaller installation."""
diff --git a/tests/project_packer_fixes/test_build_lod.py b/tests/project_packer_fixes/test_build_lod.py
index 0b18cec042..c04ae7d7cf 100644
--- a/tests/project_packer_fixes/test_build_lod.py
+++ b/tests/project_packer_fixes/test_build_lod.py
@@ -66,34 +66,34 @@ class TestBuildLoDFix:
def test_main_no_chained_path_parent_absolute(self, build_module) -> None:
"""Verify main() does not chain Path().parent.absolute() directly."""
source = inspect.getsource(build_module.main)
- assert "Path(__file__).parent.absolute()" not in source, (
- "LoD violation: build.py must not chain Path(__file__).parent.absolute()"
- )
+ assert (
+ "Path(__file__).parent.absolute()" not in source
+ ), "LoD violation: build.py must not chain Path(__file__).parent.absolute()"
def test_main_no_chained_stderr_write(self, build_module) -> None:
"""Verify main() does not chain sys.stderr.write() directly."""
source = inspect.getsource(build_module.main)
- assert "sys.stderr.write" not in source, (
- "LoD violation: build.py must not chain sys.stderr.write() directly"
- )
+ assert (
+ "sys.stderr.write" not in source
+ ), "LoD violation: build.py must not chain sys.stderr.write() directly"
def test_main_extracts_stderr_to_variable(self, build_module) -> None:
"""Verify main() extracts sys.stderr to a local variable."""
source = inspect.getsource(build_module.main)
- assert "stderr = sys.stderr" in source, (
- "build.py main() should extract sys.stderr to a local variable"
- )
+ assert (
+ "stderr = sys.stderr" in source
+ ), "build.py main() should extract sys.stderr to a local variable"
def test_main_extracts_path_parent(self, build_module) -> None:
"""Verify main() extracts Path().parent to an intermediate variable."""
source = inspect.getsource(build_module.main)
# Should use script_parent or similar intermediate variable
- assert "Path(__file__).parent" in source, (
- "build.py should still use Path(__file__).parent but assign to intermediate"
- )
- assert ".absolute()" in source, (
- "build.py should call .absolute() on the intermediate variable"
- )
+ assert (
+ "Path(__file__).parent" in source
+ ), "build.py should still use Path(__file__).parent but assign to intermediate"
+ assert (
+ ".absolute()" in source
+ ), "build.py should call .absolute() on the intermediate variable"
def test_no_print_calls_in_source(self, build_module) -> None:
"""Verify no print() calls exist in build module source."""
diff --git a/tests/project_packer_fixes/test_folder_packer_gui_lod.py b/tests/project_packer_fixes/test_folder_packer_gui_lod.py
index e7d808756c..46bbb239d4 100644
--- a/tests/project_packer_fixes/test_folder_packer_gui_lod.py
+++ b/tests/project_packer_fixes/test_folder_packer_gui_lod.py
@@ -112,18 +112,18 @@ def test_should_include_file_no_chained_suffix_lower(self, gui_module) -> None:
import inspect
source = inspect.getsource(gui_module.FolderPackerGUI.should_include_file)
- assert "file_path.suffix.lower()" not in source, (
- "LoD violation: should_include_file must not chain .suffix.lower()"
- )
+ assert (
+ "file_path.suffix.lower()" not in source
+ ), "LoD violation: should_include_file must not chain .suffix.lower()"
def test_should_include_directory_no_chained_name_lower(self, gui_module) -> None:
"""Verify should_include_directory does not use dir_path.name.lower() chain."""
import inspect
source = inspect.getsource(gui_module.FolderPackerGUI.should_include_directory)
- assert "dir_path.name.lower()" not in source, (
- "LoD violation: should_include_directory must not chain .name.lower()"
- )
+ assert (
+ "dir_path.name.lower()" not in source
+ ), "LoD violation: should_include_directory must not chain .name.lower()"
def test_should_include_file_python_file(self, gui_instance) -> None:
"""Test that .py files are included."""
@@ -203,9 +203,9 @@ def test_no_print_calls_in_source(self, gui_module) -> None:
for i, line in enumerate(lines)
if "print(" in line and not line.strip().startswith("#")
]
- assert not print_lines, (
- f"Found print() calls in folder_packer_gui.py: {print_lines}"
- )
+ assert (
+ not print_lines
+ ), f"Found print() calls in folder_packer_gui.py: {print_lines}"
class TestFolderPackerGuiDbCContracts:
diff --git a/tests/rust_bindings/test_math_primitives_bindings.py b/tests/rust_bindings/test_math_primitives_bindings.py
index 6b2d483c2a..06bc214ed3 100644
--- a/tests/rust_bindings/test_math_primitives_bindings.py
+++ b/tests/rust_bindings/test_math_primitives_bindings.py
@@ -64,9 +64,9 @@ def test_orthonormality(self) -> None:
for j in range(3):
dot = sum(r[k][i] * r[k][j] for k in range(3))
expected = 1.0 if i == j else 0.0
- assert abs(dot - expected) < 1e-10, (
- f"Orthogonality violated at ({i},{j}): {dot}"
- )
+ assert (
+ abs(dot - expected) < 1e-10
+ ), f"Orthogonality violated at ({i},{j}): {dot}"
class TestRotationMatrixToEuler:
@@ -86,9 +86,9 @@ def test_roundtrip(self, euler: list[float]) -> None:
r = mp.euler_to_rotation_matrix(euler)
recovered = mp.rotation_matrix_to_euler(r)
for i in range(3):
- assert abs(recovered[i] - euler[i]) < 1e-10, (
- f"Roundtrip failed at index {i}: {recovered[i]} != {euler[i]}"
- )
+ assert (
+ abs(recovered[i] - euler[i]) < 1e-10
+ ), f"Roundtrip failed at index {i}: {recovered[i]} != {euler[i]}"
# ---------------------------------------------------------------------------
diff --git a/tests/scripts/test_generate_tools_json.py b/tests/scripts/test_generate_tools_json.py
index 4a57d59510..f137b94df8 100644
--- a/tests/scripts/test_generate_tools_json.py
+++ b/tests/scripts/test_generate_tools_json.py
@@ -250,9 +250,9 @@ def test_contract_tool_id_format(self, manifest_gen_module, mock_repo_root):
pattern = re.compile(r"^[a-z0-9_]+$")
for tool in contract["tools"]:
- assert pattern.match(tool["id"]), (
- f"Tool ID '{tool['id']}' is not snake_case"
- )
+ assert pattern.match(
+ tool["id"]
+ ), f"Tool ID '{tool['id']}' is not snake_case"
def test_contract_surfaces_structure(self, manifest_gen_module, mock_repo_root):
"""Each tool's surfaces dict must have exactly pyqt6 and web booleans."""
@@ -334,9 +334,9 @@ def test_contract_schema_compliance(self, manifest_gen_module, mock_repo_root):
expected_surface_keys = {"pyqt6", "web", "legacy_gui"}
for tool in contract["tools"]:
- assert set(tool.keys()) == expected_tool_keys, (
- f"Unexpected keys in tool entry: {set(tool.keys()) - expected_tool_keys}"
- )
+ assert (
+ set(tool.keys()) == expected_tool_keys
+ ), f"Unexpected keys in tool entry: {set(tool.keys()) - expected_tool_keys}"
assert set(tool["surfaces"].keys()) == expected_surface_keys
diff --git a/tests/shared/python/ai/integrations/test_linear_client.py b/tests/shared/python/ai/integrations/test_linear_client.py
index 4008c3117d..6e32bc22ff 100644
--- a/tests/shared/python/ai/integrations/test_linear_client.py
+++ b/tests/shared/python/ai/integrations/test_linear_client.py
@@ -111,9 +111,11 @@ def with_token():
"""Set a dummy token for tests that need one."""
set_linear_api_token("test-token-abc")
yield
- set_linear_api_token.__wrapped__ if hasattr(
- set_linear_api_token, "__wrapped__"
- ) else None
+ (
+ set_linear_api_token.__wrapped__
+ if hasattr(set_linear_api_token, "__wrapped__")
+ else None
+ )
# ---------------------------------------------------------------------------
diff --git a/tests/shared/python/ai/test_adapter_contract.py b/tests/shared/python/ai/test_adapter_contract.py
index 86fec72c1d..03245efa57 100644
--- a/tests/shared/python/ai/test_adapter_contract.py
+++ b/tests/shared/python/ai/test_adapter_contract.py
@@ -44,18 +44,18 @@ def _assert_canonical_usage(usage: dict[str, int], adapter_name: str) -> None:
f"got {set(usage.keys())!r}"
)
for key in _CANONICAL_USAGE_KEYS:
- assert isinstance(usage[key], int), (
- f"{adapter_name}: usage['{key}'] must be int, got {type(usage[key])!r}"
- )
+ assert isinstance(
+ usage[key], int
+ ), f"{adapter_name}: usage['{key}'] must be int, got {type(usage[key])!r}"
def _assert_stream_terminates(chunks: Iterator[AgentChunk], adapter_name: str) -> None:
"""Consume *chunks* and assert at least one has ``is_final=True``."""
chunk_list = list(chunks)
finals = [c for c in chunk_list if c.is_final]
- assert finals, (
- f"{adapter_name}: stream_response did not emit any chunk with is_final=True"
- )
+ assert (
+ finals
+ ), f"{adapter_name}: stream_response did not emit any chunk with is_final=True"
# ---------------------------------------------------------------------------
diff --git a/tests/shared/python/ai/test_adapter_factory.py b/tests/shared/python/ai/test_adapter_factory.py
index 4a66c45536..75adf3238c 100644
--- a/tests/shared/python/ai/test_adapter_factory.py
+++ b/tests/shared/python/ai/test_adapter_factory.py
@@ -94,9 +94,9 @@ def test_create_different_configs_returns_different_instances() -> None:
adapter_a = AdapterFactory.create("ollama", model="llama3")
adapter_b = AdapterFactory.create("ollama", model="mistral")
- assert adapter_a is not adapter_b, (
- "Different model configurations must produce distinct adapter instances."
- )
+ assert (
+ adapter_a is not adapter_b
+ ), "Different model configurations must produce distinct adapter instances."
def test_create_different_hosts_returns_different_instances() -> None:
@@ -111,9 +111,9 @@ def test_create_different_hosts_returns_different_instances() -> None:
adapter_a = AdapterFactory.create("ollama", host="http://host-a:11434")
adapter_b = AdapterFactory.create("ollama", host="http://host-b:11434")
- assert adapter_a is not adapter_b, (
- "Different host configurations must produce distinct adapter instances."
- )
+ assert (
+ adapter_a is not adapter_b
+ ), "Different host configurations must produce distinct adapter instances."
# ---------------------------------------------------------------------------
@@ -134,9 +134,9 @@ def test_clear_cache_causes_fresh_construction() -> None:
AdapterFactory.clear_cache()
second = AdapterFactory.create("ollama", model="llama3")
- assert first is not second, (
- "After clear_cache(), create() must construct a fresh adapter instance."
- )
+ assert (
+ first is not second
+ ), "After clear_cache(), create() must construct a fresh adapter instance."
def test_clear_cache_empties_internal_dict() -> None:
@@ -149,9 +149,9 @@ def test_clear_cache_empties_internal_dict() -> None:
):
AdapterFactory.create("ollama")
- assert len(AdapterFactory._cache) == 1, (
- "Cache should have one entry after create()."
- )
+ assert (
+ len(AdapterFactory._cache) == 1
+ ), "Cache should have one entry after create()."
AdapterFactory.clear_cache()
assert len(AdapterFactory._cache) == 0, "Cache should be empty after clear_cache()."
@@ -171,6 +171,6 @@ def test_constructor_called_once_for_repeated_create() -> None:
AdapterFactory.create("ollama", model="llama3")
AdapterFactory.create("ollama", model="llama3")
- assert mock_cls.call_count == 1, (
- f"OllamaAdapter constructor should be called once, got {mock_cls.call_count}."
- )
+ assert (
+ mock_cls.call_count == 1
+ ), f"OllamaAdapter constructor should be called once, got {mock_cls.call_count}."
diff --git a/tests/shared/python/ai/test_cli_provider_setup.py b/tests/shared/python/ai/test_cli_provider_setup.py
index f9d076282d..54f097f9c8 100644
--- a/tests/shared/python/ai/test_cli_provider_setup.py
+++ b/tests/shared/python/ai/test_cli_provider_setup.py
@@ -27,9 +27,9 @@ class TestCatalogue:
def test_all_cli_providers_covered(self) -> None:
"""Every CLI-shaped provider must have an install/auth card."""
expected = {"claude_code", "codex_cli", "gemini_cli", "cline"}
- assert expected.issubset(CLI_PROVIDERS.keys()), (
- f"Missing CLI providers: {expected - set(CLI_PROVIDERS.keys())}"
- )
+ assert expected.issubset(
+ CLI_PROVIDERS.keys()
+ ), f"Missing CLI providers: {expected - set(CLI_PROVIDERS.keys())}"
@pytest.mark.parametrize(
"provider", ["claude_code", "codex_cli", "gemini_cli", "cline"]
@@ -38,12 +38,12 @@ def test_each_spec_has_required_fields(self, provider: str) -> None:
spec = CLI_PROVIDERS[provider]
assert spec.display_name, f"{provider}: empty display_name"
assert spec.install_command, f"{provider}: empty install_command"
- assert spec.install_url.startswith(("http://", "https://")), (
- f"{provider}: install_url not a URL: {spec.install_url!r}"
- )
- assert len(spec.auth_instructions) > 20, (
- f"{provider}: auth_instructions too short to be useful"
- )
+ assert spec.install_url.startswith(
+ ("http://", "https://")
+ ), f"{provider}: install_url not a URL: {spec.install_url!r}"
+ assert (
+ len(spec.auth_instructions) > 20
+ ), f"{provider}: auth_instructions too short to be useful"
class TestStatusProbe:
diff --git a/tests/shared/python/ai/test_onnx_preflight.py b/tests/shared/python/ai/test_onnx_preflight.py
index b71630c01b..253f1d2abc 100644
--- a/tests/shared/python/ai/test_onnx_preflight.py
+++ b/tests/shared/python/ai/test_onnx_preflight.py
@@ -117,9 +117,9 @@ def test_error_message_includes_os_error(
with pytest.raises(RuntimeError) as exc_info:
check_ort_loadable()
- assert exc_info.value.__cause__ is not None, (
- "RuntimeError should chain the underlying OSError"
- )
+ assert (
+ exc_info.value.__cause__ is not None
+ ), "RuntimeError should chain the underlying OSError"
def test_raises_for_nonexistent_explicit_path(self) -> None:
"""Explicit dylib_path argument is used instead of env var."""
diff --git a/tests/shared/python/ai/test_provider_config_registry.py b/tests/shared/python/ai/test_provider_config_registry.py
index 8bec7bda35..0ede248454 100644
--- a/tests/shared/python/ai/test_provider_config_registry.py
+++ b/tests/shared/python/ai/test_provider_config_registry.py
@@ -14,9 +14,9 @@
def test_default_registrations_cover_all_providers(qapp) -> None:
for provider in AIProvider:
- assert ProviderConfigRegistry.is_registered(provider.name), (
- f"missing registration for {provider}"
- )
+ assert ProviderConfigRegistry.is_registered(
+ provider.name
+ ), f"missing registration for {provider}"
def test_get_widget_returns_distinct_instances(qapp) -> None:
diff --git a/tests/shared/python/ai/test_rust_adapter_fallback.py b/tests/shared/python/ai/test_rust_adapter_fallback.py
index 43421036a2..28caa076f7 100644
--- a/tests/shared/python/ai/test_rust_adapter_fallback.py
+++ b/tests/shared/python/ai/test_rust_adapter_fallback.py
@@ -74,9 +74,9 @@ def test_warning_references_distribution_doc(
)
all_text = " ".join(str(r.message) for r in caplog.records)
- assert "rust_distribution.md" in all_text, (
- f"Expected 'rust_distribution.md' in log output, got: {all_text!r}"
- )
+ assert (
+ "rust_distribution.md" in all_text
+ ), f"Expected 'rust_distribution.md' in log output, got: {all_text!r}"
class TestGracefulDegradation:
diff --git a/tests/shared/python/calculators/conversion/test_service.py b/tests/shared/python/calculators/conversion/test_service.py
index 2f3d9a3b0c..918390040d 100644
--- a/tests/shared/python/calculators/conversion/test_service.py
+++ b/tests/shared/python/calculators/conversion/test_service.py
@@ -76,9 +76,9 @@ def test_factor_table_round_trip_exactness(service: UnitConversionService) -> No
back = service.convert(forward, other, base).value
except (IncompatibleUnitsError, TypeError, ValueError, UnknownUnitError):
continue
- assert back == pytest.approx(1.0, rel=1e-9), (
- f"{category}: {base}->{other}->{base} lost precision"
- )
+ assert back == pytest.approx(
+ 1.0, rel=1e-9
+ ), f"{category}: {base}->{other}->{base} lost precision"
checked += 1
assert checked > 0
diff --git a/tests/shared/python/chat/test_chat_agent_label.py b/tests/shared/python/chat/test_chat_agent_label.py
index 320f2210cb..2d69bd05b2 100644
--- a/tests/shared/python/chat/test_chat_agent_label.py
+++ b/tests/shared/python/chat/test_chat_agent_label.py
@@ -29,7 +29,9 @@
# ---------------------------------------------------------------------------
-def test_user_bubble_always_labelled_you(qapp) -> None: # noqa: F811 - qapp is conftest fixture
+def test_user_bubble_always_labelled_you(
+ qapp,
+) -> None: # noqa: F811 - qapp is conftest fixture
from src.shared.python.chat._qt.bubbles import ChatMessageBubble
bubble = ChatMessageBubble("user", "hi", agent_label="Agent (gpt-4o)")
diff --git a/tests/shared/python/chat/test_chat_session_helpers.py b/tests/shared/python/chat/test_chat_session_helpers.py
index 21dbff32a9..53c740ea51 100644
--- a/tests/shared/python/chat/test_chat_session_helpers.py
+++ b/tests/shared/python/chat/test_chat_session_helpers.py
@@ -103,9 +103,9 @@ def writer(sid: str) -> None:
# No leftover .tmp file after atomic replaces.
assert not (path.parent / f"{path.name}.tmp").exists()
final = path.read_text(encoding="utf-8")
- assert final in candidates, (
- f"expected exactly one of {candidates!r}, got {final!r}"
- )
+ assert (
+ final in candidates
+ ), f"expected exactly one of {candidates!r}, got {final!r}"
def test_atomic_write_leaves_no_tmp_file(self, tmp_path: Path) -> None:
"""Atomic write cleans up the .tmp file after replace."""
diff --git a/tests/shared/python/chat/test_quick_bar.py b/tests/shared/python/chat/test_quick_bar.py
index 89b3ae3d91..c2cada99a3 100644
--- a/tests/shared/python/chat/test_quick_bar.py
+++ b/tests/shared/python/chat/test_quick_bar.py
@@ -116,12 +116,14 @@ def test_all_canonical_keys_present(self) -> None:
def test_fallback_provider_produces_coherent_palette(self) -> None:
c = _resolve_colors(_FallbackThemeProvider())
for key, value in c.items():
- assert len(value) in (4, 7, 9), (
- f"Key {key!r} resolved to non-standard color: {value!r}"
- )
- assert value.startswith("#"), (
- f"Key {key!r} resolved to non-hex color: {value!r}"
- )
+ assert len(value) in (
+ 4,
+ 7,
+ 9,
+ ), f"Key {key!r} resolved to non-standard color: {value!r}"
+ assert value.startswith(
+ "#"
+ ), f"Key {key!r} resolved to non-hex color: {value!r}"
def test_partial_theme_uses_fallback_for_missing_keys(self) -> None:
"""A partial palette should not break resolution for missing tokens."""
diff --git a/tests/shared/python/chat/test_router_error_logging.py b/tests/shared/python/chat/test_router_error_logging.py
index c6f4fca2ee..87fe0c2c9b 100644
--- a/tests/shared/python/chat/test_router_error_logging.py
+++ b/tests/shared/python/chat/test_router_error_logging.py
@@ -281,7 +281,9 @@ def test_index_codebase_error_logging(
with caplog.at_level(logging.WARNING, logger="chat.router_factory"):
with client.websocket_connect("/api/ws/chat/new") as ws:
ws.receive_json()
- ws.send_json({"action": "index_codebase", "root_path": "/tmp"}) # nosec B108
+ ws.send_json(
+ {"action": "index_codebase", "root_path": "/tmp"}
+ ) # nosec B108
payload = ws.receive_json()
assert payload == {"type": "error", "detail": "disk full"}
diff --git a/tests/shared/python/chat/test_terminal_runtime.py b/tests/shared/python/chat/test_terminal_runtime.py
index 06f1d0c078..f98848b6c3 100644
--- a/tests/shared/python/chat/test_terminal_runtime.py
+++ b/tests/shared/python/chat/test_terminal_runtime.py
@@ -246,9 +246,9 @@ def test_default_session_env_excludes_credential_variables(
env = _build_default_session_env()
- assert var_name not in env, (
- f"{var_name!r} must not appear in the default session env"
- )
+ assert (
+ var_name not in env
+ ), f"{var_name!r} must not appear in the default session env"
def test_default_session_env_includes_path(monkeypatch: pytest.MonkeyPatch) -> None:
diff --git a/tests/shared/python/model_generation/test_gh1694_xml_security.py b/tests/shared/python/model_generation/test_gh1694_xml_security.py
index 297b5c4b7e..5bab798579 100644
--- a/tests/shared/python/model_generation/test_gh1694_xml_security.py
+++ b/tests/shared/python/model_generation/test_gh1694_xml_security.py
@@ -265,6 +265,6 @@ def test_validate_mjcf_no_stdlib_et_parse_in_fallback(self) -> None:
source = inspect.getsource(format_utils)
# The fallback branch must not use StdET.ParseError
- assert "StdET" not in source, (
- "format_utils.py must not reference StdET — use DefusedET.ParseError instead"
- )
+ assert (
+ "StdET" not in source
+ ), "format_utils.py must not reference StdET — use DefusedET.ParseError instead"
diff --git a/tests/shared/python/theme/test_fallback_drift.py b/tests/shared/python/theme/test_fallback_drift.py
index ae77ae3b90..dceddf1f61 100644
--- a/tests/shared/python/theme/test_fallback_drift.py
+++ b/tests/shared/python/theme/test_fallback_drift.py
@@ -25,9 +25,9 @@ def _json_themes() -> dict[str, dict[str, str]]:
def test_fallback_theme_names_match_json() -> None:
"""The fallback exposes exactly the themes defined in themes.json."""
json_themes = _json_themes()
- assert set(colors._HARDCODED_BUILTIN_THEMES) == set(json_themes), (
- "Hardcoded fallback theme set drifted from themes.json"
- )
+ assert set(colors._HARDCODED_BUILTIN_THEMES) == set(
+ json_themes
+ ), "Hardcoded fallback theme set drifted from themes.json"
def test_fallback_base_colors_match_json() -> None:
@@ -52,9 +52,9 @@ def test_chart_colors_fallback_matches_json() -> None:
json_chart = colors._load_chart_colors_from_json()
if json_chart is None:
pytest.skip("themes.json not available in this environment")
- assert colors._HARDCODED_CHART_COLORS == json_chart, (
- "Hardcoded chart-color fallback drifted from themes.json"
- )
+ assert (
+ colors._HARDCODED_CHART_COLORS == json_chart
+ ), "Hardcoded chart-color fallback drifted from themes.json"
def test_builtin_themes_is_json_derived_when_available() -> None:
diff --git a/tests/shared/python/ui/test_headless_import.py b/tests/shared/python/ui/test_headless_import.py
index 4c13a4dbb6..200db4f8bc 100644
--- a/tests/shared/python/ui/test_headless_import.py
+++ b/tests/shared/python/ui/test_headless_import.py
@@ -15,8 +15,7 @@
def test_ui_imports_without_pyqt6() -> None:
"""Importing ``ui`` succeeds with PyQt6 forced absent; widgets are None."""
- script = textwrap.dedent(
- """
+ script = textwrap.dedent("""
import sys
import importlib.abc
@@ -42,8 +41,7 @@ def find_spec(self, name, path=None, target=None):
assert "AutoCompleteLineEdit" in ui.__all__
assert "HoverCopyTextBrowser" in ui.__all__
print("HEADLESS_UI_IMPORT_OK")
- """
- )
+ """)
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
@@ -51,9 +49,9 @@ def find_spec(self, name, path=None, target=None):
check=False,
cwd=_repo_src_dir(),
)
- assert result.returncode == 0, (
- f"headless ui import failed:\nstdout={result.stdout}\nstderr={result.stderr}"
- )
+ assert (
+ result.returncode == 0
+ ), f"headless ui import failed:\nstdout={result.stdout}\nstderr={result.stderr}"
assert "HEADLESS_UI_IMPORT_OK" in result.stdout
diff --git a/tests/test_gh1655_print_to_logging.py b/tests/test_gh1655_print_to_logging.py
index c20440567b..44436efd65 100644
--- a/tests/test_gh1655_print_to_logging.py
+++ b/tests/test_gh1655_print_to_logging.py
@@ -48,9 +48,9 @@ def test_import_logging_present(self) -> None:
elif isinstance(node, ast.ImportFrom):
if node.module:
import_names.append(node.module)
- assert "logging" in import_names, (
- "logging must be imported in modern_robotics.py"
- )
+ assert (
+ "logging" in import_names
+ ), "logging must be imported in modern_robotics.py"
class TestNoUnguardedPrintInSrc:
@@ -153,9 +153,9 @@ def test_t201_in_ruff_select(self) -> None:
ruff_toml = Path(__file__).parents[1] / "ruff.toml"
config = tomllib.loads(ruff_toml.read_text())
lint_select = config["lint"]["select"]
- assert "T201" in lint_select, (
- "T201 must be in [lint] select in ruff.toml to enforce no-print policy"
- )
+ assert (
+ "T201" in lint_select
+ ), "T201 must be in [lint] select in ruff.toml to enforce no-print policy"
def test_notebooks_excluded_from_t201(self) -> None:
"""Notebooks must be excluded from T201 (print is valid)."""
diff --git a/tests/test_gh1732_logging_consistency.py b/tests/test_gh1732_logging_consistency.py
index e6c75f1010..a513104392 100644
--- a/tests/test_gh1732_logging_consistency.py
+++ b/tests/test_gh1732_logging_consistency.py
@@ -113,9 +113,9 @@ def test_collection_covers_shared_python(self) -> None:
"""Sweep includes src/shared/python — the shared library layer."""
files = _collect_library_py_files()
shared_files = [f for f in files if "shared" in f.parts and "python" in f.parts]
- assert len(shared_files) > 0, (
- "Expected at least one file from src/shared/python/ in the sweep"
- )
+ assert (
+ len(shared_files) > 0
+ ), "Expected at least one file from src/shared/python/ in the sweep"
def test_collection_excludes_ruff_excluded_dirs(self) -> None:
"""Files from ruff-excluded directories are not in the sweep."""
@@ -123,18 +123,18 @@ def test_collection_excludes_ruff_excluded_dirs(self) -> None:
for f in files:
parts = f.relative_to(_SRC_ROOT).parts
excluded = [p for p in parts if p in _RUFF_EXCLUDED_SRC_DIRS]
- assert not excluded, (
- f"File from excluded directory should not be in sweep: {f}"
- )
+ assert (
+ not excluded
+ ), f"File from excluded directory should not be in sweep: {f}"
def test_collection_excludes_test_subdirs(self) -> None:
"""Test subdirectories are not in the sweep."""
files = _collect_library_py_files()
for f in files:
parts = f.relative_to(_SRC_ROOT).parts
- assert "tests" not in parts, (
- f"File from tests/ subdirectory should not be in sweep: {f}"
- )
+ assert (
+ "tests" not in parts
+ ), f"File from tests/ subdirectory should not be in sweep: {f}"
class TestLoggingConsistencyRuffConfig:
@@ -150,9 +150,9 @@ def test_t201_in_ruff_select(self) -> None:
ruff_toml = _REPO_ROOT / "ruff.toml"
config = tomllib.loads(ruff_toml.read_text())
lint_select = config["lint"]["select"]
- assert "T201" in lint_select, (
- "T201 must be in [lint] select in ruff.toml to enforce the no-print policy"
- )
+ assert (
+ "T201" in lint_select
+ ), "T201 must be in [lint] select in ruff.toml to enforce the no-print policy"
def test_notebooks_excluded_from_t201(self) -> None:
"""Notebooks must have T201 suppressed (print is valid in notebooks)."""
diff --git a/tests/test_no_urdf_builder_root_duplicates.py b/tests/test_no_urdf_builder_root_duplicates.py
index 45d4ac2cfa..def755888f 100644
--- a/tests/test_no_urdf_builder_root_duplicates.py
+++ b/tests/test_no_urdf_builder_root_duplicates.py
@@ -75,9 +75,9 @@ def test_canonical_modules_present(self) -> None:
"preview_generator.py",
]
missing = [m for m in essential if not (_CANONICAL_PKG / m).exists()]
- assert not missing, (
- "Canonical package is missing essential modules: " + ", ".join(missing)
- )
+ assert (
+ not missing
+ ), "Canonical package is missing essential modules: " + ", ".join(missing)
def test_path_bridge_uses_insert_not_append(self) -> None:
"""__init__.py must use __path__.insert(0, …) not append (#3346).
diff --git a/tests/test_review_fixes_2026_03_09.py b/tests/test_review_fixes_2026_03_09.py
index b51be34217..f6077cc7cf 100644
--- a/tests/test_review_fixes_2026_03_09.py
+++ b/tests/test_review_fixes_2026_03_09.py
@@ -57,9 +57,9 @@ def test_points_3_channels_fills_nan_residuals(self, reader):
reader._metadata = None
df = reader.points_dataframe(include_time=False)
assert "residual" in df.columns
- assert df["residual"].isna().all(), (
- "Residuals should be NaN when only 3 channels present"
- )
+ assert (
+ df["residual"].isna().all()
+ ), "Residuals should be NaN when only 3 channels present"
def test_points_4_channels_has_residuals(self, reader):
"""When C3D has 4 channels, residuals are extracted normally."""
diff --git a/tests/test_sidekick_public_api_stability.py b/tests/test_sidekick_public_api_stability.py
index 4330060ceb..049da92468 100644
--- a/tests/test_sidekick_public_api_stability.py
+++ b/tests/test_sidekick_public_api_stability.py
@@ -254,17 +254,17 @@ def test_sidekick_public_api_stability(pytestconfig: pytest.Config) -> None:
log.info("Regenerated public API baseline in %s", BASELINE_PATH)
return
- assert BASELINE_PATH.is_file(), (
- "Baseline file not found. Run with --regenerate-api-baseline to create it."
- )
+ assert (
+ BASELINE_PATH.is_file()
+ ), "Baseline file not found. Run with --regenerate-api-baseline to create it."
with open(BASELINE_PATH, encoding="utf-8") as f:
baseline_api = json.load(f)
# Compare keys
- assert set(current_api.keys()) == set(baseline_api.keys()), (
- "Set of public sidekick module files changed."
- )
+ assert set(current_api.keys()) == set(
+ baseline_api.keys()
+ ), "Set of public sidekick module files changed."
# Perform detailed comparison to raise clean assertions
mismatches = []
diff --git a/tests/test_src_package_import_contract.py b/tests/test_src_package_import_contract.py
index 31596170d4..43cbcdbc0e 100644
--- a/tests/test_src_package_import_contract.py
+++ b/tests/test_src_package_import_contract.py
@@ -48,6 +48,6 @@ def _import_under_consumer_contract(dotted: str) -> subprocess.CompletedProcess[
def test_top_level_packages_import_under_repo_root_only(package: str) -> None:
"""``import src.`` must succeed with only the repo root on path."""
result = _import_under_consumer_contract(f"src.{package}")
- assert result.returncode == 0, (
- f"import src.{package} failed under repo-root-only sys.path:\n{result.stderr}"
- )
+ assert (
+ result.returncode == 0
+ ), f"import src.{package} failed under repo-root-only sys.path:\n{result.stderr}"
diff --git a/tests/tools/test_logger_shim.py b/tests/tools/test_logger_shim.py
index e13f51251d..bc9aed11a0 100644
--- a/tests/tools/test_logger_shim.py
+++ b/tests/tools/test_logger_shim.py
@@ -17,9 +17,9 @@ def test_logger_shim_issues_deprecation_warning():
import tools.logger # noqa: F401
dep_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)]
- assert any("tools.logger" in str(w.message) for w in dep_warnings), (
- "Expected DeprecationWarning about tools.logger"
- )
+ assert any(
+ "tools.logger" in str(w.message) for w in dep_warnings
+ ), "Expected DeprecationWarning about tools.logger"
def test_logger_shim_re_exports_setup_logging():
diff --git a/tests/unit/ai/gui/test_chat_export.py b/tests/unit/ai/gui/test_chat_export.py
index 56c8ac1f29..925a4c808c 100644
--- a/tests/unit/ai/gui/test_chat_export.py
+++ b/tests/unit/ai/gui/test_chat_export.py
@@ -318,7 +318,7 @@ def test_copy_button_visible_on_message_widget(self) -> None:
widget = MessageWidget("user", "Hello world")
assert hasattr(widget, "_copy_btn"), "MessageWidget missing _copy_btn attribute"
- assert isinstance(widget._copy_btn, QToolButton), (
- "_copy_btn must be a QToolButton"
- )
+ assert isinstance(
+ widget._copy_btn, QToolButton
+ ), "_copy_btn must be a QToolButton"
_ = app # keep reference alive
diff --git a/tests/unit/ai/integrations/github_mcp/test_tool_descriptors.py b/tests/unit/ai/integrations/github_mcp/test_tool_descriptors.py
index ebbcfdf8b6..8cfc2c6b23 100644
--- a/tests/unit/ai/integrations/github_mcp/test_tool_descriptors.py
+++ b/tests/unit/ai/integrations/github_mcp/test_tool_descriptors.py
@@ -45,13 +45,13 @@ def test_write_tools_require_confirmation() -> None:
"""Mutating tools must opt into ``requires_confirmation=True``."""
for tool in GITHUB_MCP_TOOL_DESCRIPTORS:
if tool.name in _EXPECTED_WRITE_TOOLS:
- assert tool.requires_confirmation is True, (
- f"write tool {tool.name} must require confirmation"
- )
+ assert (
+ tool.requires_confirmation is True
+ ), f"write tool {tool.name} must require confirmation"
else:
- assert tool.requires_confirmation is False, (
- f"read tool {tool.name} must not require confirmation"
- )
+ assert (
+ tool.requires_confirmation is False
+ ), f"read tool {tool.name} must not require confirmation"
def test_write_tool_names_helper() -> None:
diff --git a/tests/unit/ai/mcp/test_notebooklm_server_phase2.py b/tests/unit/ai/mcp/test_notebooklm_server_phase2.py
index 43977a0259..b63741a26d 100644
--- a/tests/unit/ai/mcp/test_notebooklm_server_phase2.py
+++ b/tests/unit/ai/mcp/test_notebooklm_server_phase2.py
@@ -90,9 +90,9 @@ def test_phase2_confirmation_tools_have_metadata() -> None:
tools_by_name = {tool["name"]: tool for tool in response["result"]["tools"]}
for needs_confirm in ("generate_audio_overview", "attach_to_chat"):
meta = tools_by_name[needs_confirm].get("metadata") or {}
- assert meta.get("requires_confirmation") is True, (
- f"{needs_confirm} must declare requires_confirmation=True"
- )
+ assert (
+ meta.get("requires_confirmation") is True
+ ), f"{needs_confirm} must declare requires_confirmation=True"
# ---------------------------------------------------------------------------
diff --git a/tests/unit/ai/test_peer_review.py b/tests/unit/ai/test_peer_review.py
index 2eddeb5421..9388c65e1d 100644
--- a/tests/unit/ai/test_peer_review.py
+++ b/tests/unit/ai/test_peer_review.py
@@ -299,9 +299,9 @@ def test_dialog_has_model_selector(self) -> None:
dlg = self._dialog_cls()
combos = dlg.findChildren(QComboBox)
- assert len(combos) >= 2, (
- "Dialog must have at least two QComboBoxes (provider + model)"
- )
+ assert (
+ len(combos) >= 2
+ ), "Dialog must have at least two QComboBoxes (provider + model)"
dlg.close()
def test_dialog_returns_selected_config(self) -> None:
@@ -311,8 +311,8 @@ def test_dialog_returns_selected_config(self) -> None:
assert isinstance(config, tuple), "get_config() must return a tuple"
assert len(config) == 2, "get_config() must return (provider, model)"
provider, model = config
- assert isinstance(provider, str) and provider, (
- "provider must be a non-empty str"
- )
+ assert (
+ isinstance(provider, str) and provider
+ ), "provider must be a non-empty str"
assert isinstance(model, str) and model, "model must be a non-empty str"
dlg.close()
diff --git a/tests/unit/chat/test_adapter_capabilities.py b/tests/unit/chat/test_adapter_capabilities.py
index b61b528f30..9ee07414be 100644
--- a/tests/unit/chat/test_adapter_capabilities.py
+++ b/tests/unit/chat/test_adapter_capabilities.py
@@ -259,14 +259,14 @@ def test_list_models_returns_non_empty_list_of_strings(
) -> None:
adapter = factory()
models = adapter.list_models()
- assert isinstance(models, list), (
- f"{provider_name}: list_models() must return a list"
- )
+ assert isinstance(
+ models, list
+ ), f"{provider_name}: list_models() must return a list"
assert models, f"{provider_name}: list_models() must not be empty"
for entry in models:
- assert isinstance(entry, str) and entry.strip(), (
- f"{provider_name}: every model id must be a non-empty string"
- )
+ assert (
+ isinstance(entry, str) and entry.strip()
+ ), f"{provider_name}: every model id must be a non-empty string"
def test_list_models_is_offline_safe(self, provider_name: str, factory) -> None:
"""``list_models()`` must fall back to a static catalogue when the
@@ -286,9 +286,9 @@ def test_thinking_capabilities_returns_dataclass(
assert caps.provider
# Must always include at least the "none" level.
names = caps.level_names()
- assert "none" in names, (
- f"{provider_name}: thinking_capabilities must include 'none'"
- )
+ assert (
+ "none" in names
+ ), f"{provider_name}: thinking_capabilities must include 'none'"
assert caps.default_level_name in names
def test_thinking_capabilities_default_resolvable(
diff --git a/tests/unit/codemap/test_codemap_db.py b/tests/unit/codemap/test_codemap_db.py
index 0710346beb..ef83b67cde 100644
--- a/tests/unit/codemap/test_codemap_db.py
+++ b/tests/unit/codemap/test_codemap_db.py
@@ -118,8 +118,7 @@ def test_init_schema_migrates_legacy_fts_alias_schema() -> None:
conn = sqlite3.connect(":memory:")
try:
- conn.executescript(
- """
+ conn.executescript("""
CREATE TABLE meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
@@ -158,8 +157,7 @@ def test_init_schema_migrates_legacy_fts_alias_schema() -> None:
CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN
SELECT 1;
END;
- """
- )
+ """)
codemap_db.init_schema(conn)
diff --git a/tests/unit/lower_body_model/test_builder.py b/tests/unit/lower_body_model/test_builder.py
index ed1a0df176..a5f689456e 100644
--- a/tests/unit/lower_body_model/test_builder.py
+++ b/tests/unit/lower_body_model/test_builder.py
@@ -24,9 +24,9 @@ def test_build_lower_body_xml_generates_valid_mjcf() -> None:
]
# We expect floating base / pelvis joints, hip, knee, ankle joints
- assert "r_hip_x" in joint_names or "r_hip" in joint_names, (
- "Should have right hip joint"
- )
+ assert (
+ "r_hip_x" in joint_names or "r_hip" in joint_names
+ ), "Should have right hip joint"
assert "r_knee" in joint_names, "Should have right knee joint"
assert "l_knee" in joint_names, "Should have left knee joint"
@@ -137,9 +137,9 @@ def names_by_prefix(obj_type: int, count: int, prefix: str) -> set[str]:
):
r_names = names_by_prefix(obj_type, count, "r_")
l_names = names_by_prefix(obj_type, count, "l_")
- assert r_names == l_names, (
- f"obj_type={obj_type}: mismatch r={r_names} l={l_names}"
- )
+ assert (
+ r_names == l_names
+ ), f"obj_type={obj_type}: mismatch r={r_names} l={l_names}"
def test_builder_total_body_and_joint_counts() -> None:
diff --git a/tests/unit/lower_body_model/test_hip_rotation_target.py b/tests/unit/lower_body_model/test_hip_rotation_target.py
index d3eb8d965d..4c0cb334a9 100644
--- a/tests/unit/lower_body_model/test_hip_rotation_target.py
+++ b/tests/unit/lower_body_model/test_hip_rotation_target.py
@@ -120,9 +120,9 @@ def test_simulator_pelvis_driver_tracks_lateral_shift() -> None:
final_y = float(sim.data.xpos[sim.pelvis_body_id][1])
# The pelvis should have shifted in +Y during the downswing phase.
- assert final_y - initial_y > 0.01, (
- f"expected +Y shift; got {final_y - initial_y:.4f}"
- )
+ assert (
+ final_y - initial_y > 0.01
+ ), f"expected +Y shift; got {final_y - initial_y:.4f}"
def test_set_pelvis_inclined_rotation_rejects_bad_gains() -> None:
diff --git a/tests/unit/lower_body_model/test_simulator.py b/tests/unit/lower_body_model/test_simulator.py
index 55eab36fdd..80fbadf9a7 100644
--- a/tests/unit/lower_body_model/test_simulator.py
+++ b/tests/unit/lower_body_model/test_simulator.py
@@ -80,9 +80,9 @@ def test_induced_acceleration_analysis(simulator: LowerBodySimulator) -> None:
# The total induced acceleration shouldn't be identically perfectly zero
total_accel = sum(abs(v) for v in iaa_result.values())
- assert total_accel > 1e-4, (
- "Applied torque should induce some acceleration on the root body."
- )
+ assert (
+ total_accel > 1e-4
+ ), "Applied torque should induce some acceleration on the root body."
def test_history_recording_and_restoring(simulator: LowerBodySimulator) -> None:
diff --git a/tests/unit/rust/test_ai_backend_workspace.py b/tests/unit/rust/test_ai_backend_workspace.py
index d08a5069ab..a958edd65a 100644
--- a/tests/unit/rust/test_ai_backend_workspace.py
+++ b/tests/unit/rust/test_ai_backend_workspace.py
@@ -21,9 +21,9 @@
def test_ai_backend_in_cargo_workspace():
"""ai_backend must be a declared workspace member in the root Cargo.toml."""
cargo_toml = (REPO_ROOT / "Cargo.toml").read_text(encoding="utf-8")
- assert "ai_backend" in cargo_toml, (
- "rust_core/ai_backend is not listed in the root workspace Cargo.toml members"
- )
+ assert (
+ "ai_backend" in cargo_toml
+ ), "rust_core/ai_backend is not listed in the root workspace Cargo.toml members"
@pytest.mark.unit
@@ -62,12 +62,12 @@ def test_maturin_ci_covers_all_platforms():
for wf_path in candidates:
content = wf_path.read_text(encoding="utf-8").lower()
assert "windows" in content, f"{wf_path.name}: missing Windows runner"
- assert "ubuntu" in content or "linux" in content, (
- f"{wf_path.name}: missing Ubuntu/Linux runner"
- )
- assert "macos" in content or "mac" in content, (
- f"{wf_path.name}: missing macOS runner"
- )
+ assert (
+ "ubuntu" in content or "linux" in content
+ ), f"{wf_path.name}: missing Ubuntu/Linux runner"
+ assert (
+ "macos" in content or "mac" in content
+ ), f"{wf_path.name}: missing macOS runner"
@pytest.mark.unit
@@ -79,9 +79,9 @@ def test_maturin_ci_covers_python_versions():
+ list(workflows_dir.glob("*ai_backend*"))
+ list(workflows_dir.glob("*ai-backend*"))
)
- assert candidates, (
- "No maturin CI workflow found — cannot check Python version coverage."
- )
+ assert (
+ candidates
+ ), "No maturin CI workflow found — cannot check Python version coverage."
fleet_toolcache_limited = {
"maturin-data-processor-core.yml",
@@ -94,9 +94,9 @@ def test_maturin_ci_covers_python_versions():
for version in ["3.10", "3.11", "3.12"]:
assert version in content, f"Python {version} not listed in {wf_path.name}"
if wf_path.name in fleet_toolcache_limited:
- assert "3.13" in content, (
- f"{wf_path.name}: must document why Python 3.13 is not hard-gated"
- )
+ assert (
+ "3.13" in content
+ ), f"{wf_path.name}: must document why Python 3.13 is not hard-gated"
assert "toolcache" in content.lower(), (
f"{wf_path.name}: Python 3.13 deferral must cite runner "
"toolcache limits"
@@ -133,9 +133,9 @@ def test_ai_backend_cargo_toml_declares_local_embeddings_feature():
crate_toml = (REPO_ROOT / "rust_core" / "ai_backend" / "Cargo.toml").read_text(
encoding="utf-8"
)
- assert "local-embeddings" in crate_toml, (
- "rust_core/ai_backend/Cargo.toml does not declare 'local-embeddings' feature."
- )
+ assert (
+ "local-embeddings" in crate_toml
+ ), "rust_core/ai_backend/Cargo.toml does not declare 'local-embeddings' feature."
@pytest.mark.unit
diff --git a/tests/unit/sidekick/agent/test_action_audit.py b/tests/unit/sidekick/agent/test_action_audit.py
index 51274c7b3a..4d5a0f0abb 100644
--- a/tests/unit/sidekick/agent/test_action_audit.py
+++ b/tests/unit/sidekick/agent/test_action_audit.py
@@ -20,7 +20,9 @@
def _call(**params: Any) -> RecordedCall:
return RecordedCall(
- timestamp=datetime(2026, 1, 2, tzinfo=timezone.utc), # noqa: UP017 - Python 3.10 CI lacks datetime.UTC.
+ timestamp=datetime(
+ 2026, 1, 2, tzinfo=timezone.utc
+ ), # noqa: UP017 - Python 3.10 CI lacks datetime.UTC.
action_id="test.echo",
params=params,
descriptor=ActionDescriptor(
diff --git a/tests/unit/sidekick/agent/test_feature_catalog.py b/tests/unit/sidekick/agent/test_feature_catalog.py
index 6987b0b3a1..92a82535f0 100644
--- a/tests/unit/sidekick/agent/test_feature_catalog.py
+++ b/tests/unit/sidekick/agent/test_feature_catalog.py
@@ -116,7 +116,9 @@ def test_discovery_helpers_extract_metadata_and_walk_fake_package(
),
)
- walked = list(discovery._walk_package("sidekick.calculators", "calculator")) # noqa: SLF001
+ walked = list(
+ discovery._walk_package("sidekick.calculators", "calculator")
+ ) # noqa: SLF001
assert walked[0].feature_id == "calculator.fake_module"
assert walked[0].title == "Fake Module"
@@ -133,7 +135,9 @@ def test_workflow_and_importability_discovery(monkeypatch: pytest.MonkeyPatch) -
workflows = discovery._discover_workflows() # noqa: SLF001
assert workflows[0].feature_id == "workflow.build"
- assert discovery._discover_theme()[0].feature_id == "theme.sidekick_tokens" # noqa: SLF001
+ assert (
+ discovery._discover_theme()[0].feature_id == "theme.sidekick_tokens"
+ ) # noqa: SLF001
assert tuple(src.__name__ for src in discovery.discover_sources()) == (
"_discover_calculators",
"_discover_process_calculators",
diff --git a/tests/unit/sidekick/test_chat_redock.py b/tests/unit/sidekick/test_chat_redock.py
index 17535835f6..f8f564adb5 100644
--- a/tests/unit/sidekick/test_chat_redock.py
+++ b/tests/unit/sidekick/test_chat_redock.py
@@ -106,9 +106,9 @@ def test_chat_popout_window_has_redock_button(qtbot) -> None: # type: ignore[no
)
qtbot.addWidget(win)
redock_btn = win.findChild(QPushButton, _REDOCK_BUTTON_OBJECT_NAME)
- assert redock_btn is not None, (
- f"Expected QPushButton with objectName {_REDOCK_BUTTON_OBJECT_NAME!r}"
- )
+ assert (
+ redock_btn is not None
+ ), f"Expected QPushButton with objectName {_REDOCK_BUTTON_OBJECT_NAME!r}"
def test_chat_popout_window_redock_invokes_callback(qtbot) -> None: # type: ignore[no-untyped-def]
diff --git a/tests/unit/sidekick/test_sidekick_f4_collaborators.py b/tests/unit/sidekick/test_sidekick_f4_collaborators.py
index 6281dc1ddd..158afef153 100644
--- a/tests/unit/sidekick/test_sidekick_f4_collaborators.py
+++ b/tests/unit/sidekick/test_sidekick_f4_collaborators.py
@@ -85,9 +85,9 @@ def test_set_definitions_mutates_in_place(self, qtbot: Any) -> None:
[SidebarTabDefinition(tab_id="chat", title="Chat", factory=lambda *_: None)]
)
- assert alias is col._tab_definitions, ( # noqa: SLF001
- "set_definitions() must not rebind the backing dict"
- )
+ assert (
+ alias is col._tab_definitions
+ ), "set_definitions() must not rebind the backing dict" # noqa: SLF001
assert "chat" in alias, "alias must observe the new definition in place"
assert col.definition_for("chat") is not None
@@ -105,9 +105,9 @@ def test_sync_order_mutates_ids_in_place(self, qtbot: Any) -> None:
col.sync_order_from_widget()
- assert alias is col._tab_ids, ( # noqa: SLF001
- "sync_order_from_widget() must not rebind the backing list"
- )
+ assert (
+ alias is col._tab_ids
+ ), "sync_order_from_widget() must not rebind the backing list" # noqa: SLF001
assert alias == ["a", "b"], "alias must observe current visual order"
def test_add_duplicate_raises(self, qtbot: Any) -> None:
@@ -153,9 +153,9 @@ def test_replace_swaps_widget(self, qtbot: Any) -> None:
result = col.replace(old_w, new_w)
assert result is True, "replace() must return True"
- assert col.widget_for("chat") is new_w, (
- "widget_for() must return the new widget after replace()"
- )
+ assert (
+ col.widget_for("chat") is new_w
+ ), "widget_for() must return the new widget after replace()"
assert "chat" in col.visible_ids(), "id must still be in visible_ids()"
def test_clear_resets_state(self, qtbot: Any) -> None:
@@ -170,9 +170,9 @@ def test_clear_resets_state(self, qtbot: Any) -> None:
col.clear()
assert col.visible_ids() == [], "visible_ids() must be empty after clear()"
- assert col.widget_for("t") is None, (
- "widget_for() must return None after clear()"
- )
+ assert (
+ col.widget_for("t") is None
+ ), "widget_for() must return None after clear()"
def test_contains_and_index_of(self, qtbot: Any) -> None:
"""contains() and index_of() must reflect actual id list."""
@@ -250,7 +250,9 @@ def test_toggle_collapsed_hides_tabs(self, qtbot: Any) -> None:
assert not ctrl.is_collapsed, "starts expanded"
ctrl.toggle_collapsed()
assert ctrl.is_collapsed, "must be collapsed after toggle"
- assert ctrl._tabs.isVisible() is False, "tabs must be hidden when collapsed" # noqa: SLF001
+ assert (
+ ctrl._tabs.isVisible() is False
+ ), "tabs must be hidden when collapsed" # noqa: SLF001
def test_toggle_collapsed_shows_tabs_on_expand(self, qtbot: Any) -> None:
"""A second toggle_collapsed() must restore the tabs to visible."""
@@ -259,7 +261,9 @@ def test_toggle_collapsed_shows_tabs_on_expand(self, qtbot: Any) -> None:
ctrl.toggle_collapsed() # expand
assert not ctrl.is_collapsed, "must be expanded after double toggle"
- assert ctrl._tabs.isVisible() is True, "tabs must be visible after expanding" # noqa: SLF001
+ assert (
+ ctrl._tabs.isVisible() is True
+ ), "tabs must be visible after expanding" # noqa: SLF001
def test_dock_widget_is_none_before_install(self, qtbot: Any) -> None:
"""dock_widget must be None before install_as_dock() is called."""
@@ -382,6 +386,8 @@ def test_two_projects_use_different_keys(self, tmp_path: Any) -> None:
vp_b = VisibilityPersistence(project_root=root_b)
# Access the private key to assert isolation (white-box)
- assert vp_a._key != vp_b._key, ( # noqa: SLF001
+ assert (
+ vp_a._key != vp_b._key
+ ), ( # noqa: SLF001
"Different roots must produce different QSettings keys (F5 isolation)"
)
diff --git a/tests/unit/sidekick/test_sidekick_ux_hardening.py b/tests/unit/sidekick/test_sidekick_ux_hardening.py
index be4cc52be8..13bab2a0c0 100644
--- a/tests/unit/sidekick/test_sidekick_ux_hardening.py
+++ b/tests/unit/sidekick/test_sidekick_ux_hardening.py
@@ -47,7 +47,9 @@ def test_submit_sends_single_newline( # noqa: ANN201
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
written: list[bytes] = []
@@ -113,7 +115,9 @@ def _make_widget( # noqa: ANN202
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
widget = SidekickOsTerminalWidget(
project_root=tmp_path,
shells=[
@@ -169,9 +173,9 @@ def test_persist_helper_exists(self) -> None:
except ImportError:
pytest.skip("sidekick unavailable")
- assert hasattr(UnifiedToolsSidebar, "_persist_visible_tabs"), (
- "_persist_visible_tabs helper missing (F5 regression)"
- )
+ assert hasattr(
+ UnifiedToolsSidebar, "_persist_visible_tabs"
+ ), "_persist_visible_tabs helper missing (F5 regression)"
def test_qs_constants_are_defined(self) -> None:
"""Module-level QSettings constants must be present."""
@@ -182,9 +186,9 @@ def test_qs_constants_are_defined(self) -> None:
assert hasattr(sb, "_QS_ORG"), "_QS_ORG constant missing"
assert hasattr(sb, "_QS_APP"), "_QS_APP constant missing"
- assert hasattr(sb, "_QS_VISIBLE_TABS_KEY"), ( # noqa: E501
- "_QS_VISIBLE_TABS_KEY constant missing"
- )
+ assert hasattr(
+ sb, "_QS_VISIBLE_TABS_KEY"
+ ), "_QS_VISIBLE_TABS_KEY constant missing" # noqa: E501
def test_persist_uses_explicit_org_app( # noqa: ANN201
self, tmp_path: Path, qtbot: Any
@@ -198,7 +202,9 @@ def test_persist_uses_explicit_org_app( # noqa: ANN201
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
written: dict[str, Any] = {}
class _FakeQSettings:
@@ -210,7 +216,9 @@ def setValue(self, key: str, value: Any) -> None: # noqa: N802
written["key"] = key
written["value"] = value
- def value(self, key: str, default: Any = None, **kwargs: Any) -> Any: # noqa: N802
+ def value(
+ self, key: str, default: Any = None, **kwargs: Any
+ ) -> Any: # noqa: N802
return default
def sync(self) -> None: # noqa: N802
@@ -265,7 +273,9 @@ def test_second_call_raises_existing_dialog( # noqa: ANN201
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
sidebar = UnifiedToolsSidebar(project_root=tmp_path)
qtbot.addWidget(sidebar)
@@ -321,15 +331,15 @@ def test_quick_access_methods_exist(self) -> None:
except ImportError:
pytest.skip("sidekick unavailable")
- assert hasattr(ProjectFileExplorer, "_restore_quick_access"), (
- "_restore_quick_access missing (F10 regression)"
- )
- assert hasattr(ProjectFileExplorer, "_save_quick_access"), (
- "_save_quick_access missing (F10 regression)"
- )
- assert hasattr(ProjectFileExplorer, "_quick_access_settings_key"), (
- "_quick_access_settings_key missing (F10 regression)"
- )
+ assert hasattr(
+ ProjectFileExplorer, "_restore_quick_access"
+ ), "_restore_quick_access missing (F10 regression)"
+ assert hasattr(
+ ProjectFileExplorer, "_save_quick_access"
+ ), "_save_quick_access missing (F10 regression)"
+ assert hasattr(
+ ProjectFileExplorer, "_quick_access_settings_key"
+ ), "_quick_access_settings_key missing (F10 regression)"
def test_add_to_quick_access_rejects_duplicates( # noqa: ANN201
self, tmp_path: Path, qtbot: Any
@@ -343,7 +353,9 @@ def test_add_to_quick_access_rejects_duplicates( # noqa: ANN201
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
explorer = ProjectFileExplorer(project_root=tmp_path, parent=None)
qtbot.addWidget(explorer)
@@ -464,9 +476,9 @@ def test_replace_tab_widget_exists(self) -> None:
except ImportError:
pytest.skip("sidekick unavailable")
- assert hasattr(UnifiedToolsSidebar, "replace_tab_widget"), (
- "replace_tab_widget public method missing (F8 regression)"
- )
+ assert hasattr(
+ UnifiedToolsSidebar, "replace_tab_widget"
+ ), "replace_tab_widget public method missing (F8 regression)"
assert callable(UnifiedToolsSidebar.replace_tab_widget)
def test_replace_tab_widget_updates_map(self, tmp_path: Path, qtbot: Any) -> None:
@@ -479,7 +491,9 @@ def test_replace_tab_widget_updates_map(self, tmp_path: Path, qtbot: Any) -> Non
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
sidebar = UnifiedToolsSidebar(project_root=tmp_path)
qtbot.addWidget(sidebar)
@@ -493,7 +507,9 @@ def test_replace_tab_widget_updates_map(self, tmp_path: Path, qtbot: Any) -> Non
result = sidebar.replace_tab_widget(old_widget, new_widget)
assert result is True, "replace_tab_widget returned False unexpectedly"
- assert sidebar._tab_widgets.get("swap_test") is new_widget, ( # noqa: SLF001
+ assert (
+ sidebar._tab_widgets.get("swap_test") is new_widget
+ ), ( # noqa: SLF001
"_tab_widgets still points to old_widget after swap (F8 regression)"
)
@@ -509,7 +525,9 @@ def test_replace_tab_widget_returns_false_for_unknown(
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
sidebar = UnifiedToolsSidebar(project_root=tmp_path)
qtbot.addWidget(sidebar)
@@ -544,12 +562,12 @@ def test_update_from_notifies_subscribers(self) -> None:
target.update_from(source)
notified_names = {name for _, name in events}
- assert "x" in notified_names, (
- "Subscriber was not notified for 'x' (F9 regression)"
- )
- assert "y" in notified_names, (
- "Subscriber was not notified for 'y' (F9 regression)"
- )
+ assert (
+ "x" in notified_names
+ ), "Subscriber was not notified for 'x' (F9 regression)"
+ assert (
+ "y" in notified_names
+ ), "Subscriber was not notified for 'y' (F9 regression)"
def test_update_from_validates_names(self) -> None:
"""update_from must reject invalid variable names from the source."""
@@ -583,9 +601,9 @@ def test_update_from_replace_clears_existing(self) -> None:
target.update_from(source, replace=True)
- assert target.list_names() == ["new"], (
- "replace=True did not clear existing variables (F9 regression)"
- )
+ assert target.list_names() == [
+ "new"
+ ], "replace=True did not clear existing variables (F9 regression)"
def test_repr_only_entries_are_merged_and_notified(self) -> None:
"""Repr-only entries from a loaded registry must be merged + notify fired."""
@@ -611,12 +629,12 @@ def test_repr_only_entries_are_merged_and_notified(self) -> None:
target.update_from(source)
- assert "arr" in target.list_names(), (
- "repr-only variable not merged by update_from (F9 regression)"
- )
- assert "arr" in events, (
- "Subscriber not notified for repr-only variable (F9 regression)"
- )
+ assert (
+ "arr" in target.list_names()
+ ), "repr-only variable not merged by update_from (F9 regression)"
+ assert (
+ "arr" in events
+ ), "Subscriber not notified for repr-only variable (F9 regression)"
# ---------------------------------------------------------------------------
@@ -640,7 +658,9 @@ def _make_widget_with_fake_backend(
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
written: list[bytes] = []
@@ -684,9 +704,9 @@ def test_send_interrupt_writes_etx(self, tmp_path: Path, qtbot: Any) -> None:
widget, written = self._make_widget_with_fake_backend(tmp_path, qtbot)
widget._send_interrupt() # noqa: SLF001
assert written, "_send_interrupt did not write anything to backend"
- assert written[0] == b"\x03", (
- f"Expected b'\\x03' but got {written[0]!r} (F2 regression)"
- )
+ assert (
+ written[0] == b"\x03"
+ ), f"Expected b'\\x03' but got {written[0]!r} (F2 regression)"
def test_history_records_submitted_commands(
self, tmp_path: Path, qtbot: Any
@@ -698,7 +718,9 @@ def test_history_records_submitted_commands(
widget._input.setText("pwd") # noqa: SLF001
widget._on_submit() # noqa: SLF001
- assert widget._history[0] == "pwd", ( # noqa: SLF001
+ assert (
+ widget._history[0] == "pwd"
+ ), ( # noqa: SLF001
"Most recent command must be first in history (F2 regression)"
)
assert widget._history[1] == "ls -la" # noqa: SLF001
@@ -711,9 +733,9 @@ def test_history_rejects_exact_duplicates(self, tmp_path: Path, qtbot: Any) -> N
widget._input.setText("echo hi") # noqa: SLF001
widget._on_submit() # noqa: SLF001
- assert widget._history.count("echo hi") == 1, ( # noqa: SLF001
- "Duplicate command was added to history (F2 regression)"
- )
+ assert (
+ widget._history.count("echo hi") == 1
+ ), "Duplicate command was added to history (F2 regression)" # noqa: SLF001
def test_navigate_history_older(self, tmp_path: Path, qtbot: Any) -> None:
"""Up-arrow (direction=1) must populate the input with older commands."""
@@ -725,15 +747,17 @@ def test_navigate_history_older(self, tmp_path: Path, qtbot: Any) -> None:
# Navigate one step back (most recent = "second")
widget._navigate_history(direction=1) # noqa: SLF001
- assert widget._input.text() == "second", ( # noqa: SLF001
+ assert (
+ widget._input.text() == "second"
+ ), ( # noqa: SLF001
"First up-arrow should show most recent command (F2 regression)"
)
# Navigate one more step back (older = "first")
widget._navigate_history(direction=1) # noqa: SLF001
- assert widget._input.text() == "first", ( # noqa: SLF001
- "Second up-arrow should show older command (F2 regression)"
- )
+ assert (
+ widget._input.text() == "first"
+ ), "Second up-arrow should show older command (F2 regression)" # noqa: SLF001
def test_navigate_history_forward_restores_scratch(
self, tmp_path: Path, qtbot: Any
@@ -747,7 +771,9 @@ def test_navigate_history_forward_restores_scratch(
widget._navigate_history(direction=1) # noqa: SLF001 # go back
widget._navigate_history(direction=-1) # noqa: SLF001 # come forward
- assert widget._input.text() == "new draft", ( # noqa: SLF001
+ assert (
+ widget._input.text() == "new draft"
+ ), ( # noqa: SLF001
"Navigating forward past newest should restore live draft (F2 regression)"
)
@@ -774,7 +800,9 @@ def _make_repl(self, qtbot: Any) -> Any:
except ImportError:
pytest.skip("Qt/sidekick unavailable")
- app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) # noqa: F841
+ app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(
+ []
+ ) # noqa: F841
reg = WorkspaceRegistry()
widget = runtime_tabs.PythonReplWidget(
registry=reg,
@@ -787,23 +815,23 @@ def _make_repl(self, qtbot: Any) -> Any:
def test_cancel_button_present_and_hidden(self, qtbot: Any) -> None:
"""Widget must expose _cancel_button, initially hidden and disabled."""
widget = self._make_repl(qtbot)
- assert hasattr(widget, "_cancel_button"), (
- "_cancel_button missing (F6 regression)"
- )
- assert not widget._cancel_button.isVisible(), ( # noqa: SLF001
- "_cancel_button should be hidden at rest (F6 regression)"
- )
- assert not widget._cancel_button.isEnabled(), ( # noqa: SLF001
- "_cancel_button should be disabled at rest (F6 regression)"
- )
+ assert hasattr(
+ widget, "_cancel_button"
+ ), "_cancel_button missing (F6 regression)"
+ assert (
+ not widget._cancel_button.isVisible()
+ ), "_cancel_button should be hidden at rest (F6 regression)" # noqa: SLF001
+ assert (
+ not widget._cancel_button.isEnabled()
+ ), "_cancel_button should be disabled at rest (F6 regression)" # noqa: SLF001
def test_status_label_present_and_hidden(self, qtbot: Any) -> None:
"""Widget must expose _status_label, initially hidden."""
widget = self._make_repl(qtbot)
assert hasattr(widget, "_status_label"), "_status_label missing (F6 regression)"
- assert not widget._status_label.isVisible(), ( # noqa: SLF001
- "_status_label should be hidden at rest (F6 regression)"
- )
+ assert (
+ not widget._status_label.isVisible()
+ ), "_status_label should be hidden at rest (F6 regression)" # noqa: SLF001
def test_execute_completes_and_shows_output(self, qtbot: Any) -> None:
"""execute() must complete and write output to the output pane."""
@@ -823,23 +851,27 @@ def test_set_running_toggles_controls(self, qtbot: Any) -> None:
widget = self._make_repl(qtbot)
widget._set_running(True) # noqa: SLF001
- assert not widget._run_button.isEnabled(), ( # noqa: SLF001
- "Run button must be disabled while running (F6 regression)"
- )
+ assert (
+ not widget._run_button.isEnabled()
+ ), "Run button must be disabled while running (F6 regression)" # noqa: SLF001
# In headless tests the top-level window is never shown, so isVisible()
# returns False even after setVisible(True). isHidden() checks the
# widget's own explicit visibility bit, which is reliable here.
- assert not widget._cancel_button.isHidden(), ( # noqa: SLF001
+ assert (
+ not widget._cancel_button.isHidden()
+ ), ( # noqa: SLF001
"Cancel button must not be hidden while running (F6 regression)"
)
- assert not widget._status_label.isHidden(), ( # noqa: SLF001
+ assert (
+ not widget._status_label.isHidden()
+ ), ( # noqa: SLF001
"Status label must not be hidden while running (F6 regression)"
)
widget._set_running(False) # noqa: SLF001
- assert widget._run_button.isEnabled(), ( # noqa: SLF001
- "Run button must re-enable after stop (F6 regression)"
- )
- assert widget._cancel_button.isHidden(), ( # noqa: SLF001
- "Cancel button must be hidden after stop (F6 regression)"
- )
+ assert (
+ widget._run_button.isEnabled()
+ ), "Run button must re-enable after stop (F6 regression)" # noqa: SLF001
+ assert (
+ widget._cancel_button.isHidden()
+ ), "Cancel button must be hidden after stop (F6 regression)" # noqa: SLF001
diff --git a/tests/unit/sidekick/test_tab_context_menu.py b/tests/unit/sidekick/test_tab_context_menu.py
index b59208657e..2a2c12a1ff 100644
--- a/tests/unit/sidekick/test_tab_context_menu.py
+++ b/tests/unit/sidekick/test_tab_context_menu.py
@@ -249,9 +249,9 @@ def test_context_menu_has_minimize_action(tmp_path: Path, qtbot: Any) -> None:
menu = build_tab_context_menu(sidebar, tab_id)
qtbot.addWidget(menu)
action_texts = {a.text() for a in menu.actions() if a.text()}
- assert "Minimize Sidebar" in action_texts, (
- f"Expected 'Minimize Sidebar' in {action_texts}"
- )
+ assert (
+ "Minimize Sidebar" in action_texts
+ ), f"Expected 'Minimize Sidebar' in {action_texts}"
# ---------------------------------------------------------------------------
diff --git a/tests/unit/test_check_coverage_policy.py b/tests/unit/test_check_coverage_policy.py
index d997408997..ebffc6c121 100644
--- a/tests/unit/test_check_coverage_policy.py
+++ b/tests/unit/test_check_coverage_policy.py
@@ -195,7 +195,9 @@ def test_large_consolidation_branch_skips_changed_test_expansion() -> None:
run_tests_block = workflow.split(
"- name: Run Tests with Coverage",
maxsplit=1,
- )[1].split("- name: Provider-Contract Suite", maxsplit=1)[0]
+ )[
+ 1
+ ].split("- name: Provider-Contract Suite", maxsplit=1)[0]
assert "large_consolidation_branch=false" in run_tests_block
assert 'BRANCH_NAME" = "consolidate/open-prs-20260620' in run_tests_block
diff --git a/tests/unit/test_check_sidekick_coverage.py b/tests/unit/test_check_sidekick_coverage.py
index e1b85e8878..f204397ecc 100644
--- a/tests/unit/test_check_sidekick_coverage.py
+++ b/tests/unit/test_check_sidekick_coverage.py
@@ -13,16 +13,13 @@ def _line_xml(hits_by_line: list[int]) -> str:
for idx, hits in enumerate(hits_by_line, 1)
)
- class_xml = "\n".join(
- f"""
+ class_xml = "\n".join(f"""
{_line_xml(hits_by_line)}
- """
- for filename, hits_by_line in classes
- )
+ """ for filename, hits_by_line in classes)
path.write_text(
f"""
diff --git a/tests/unit/test_epic_2661_children_verification.py b/tests/unit/test_epic_2661_children_verification.py
index 3df22bc501..79a0e045d8 100644
--- a/tests/unit/test_epic_2661_children_verification.py
+++ b/tests/unit/test_epic_2661_children_verification.py
@@ -47,12 +47,12 @@ def _exists(rel_path: str) -> bool:
@pytest.mark.unit
def test_2662_tab_context_menus() -> None:
"""#2662: Tab workflow controls moved to right-click menus."""
- assert (SIDEBAR / "tab_context_menu.py").is_file(), (
- "tab_context_menu.py missing — #2662 may be phantom-closed"
- )
- assert (SIDEBAR / "tab_context_menu.py").stat().st_size > 500, (
- "tab_context_menu.py appears to be a stub (< 500 bytes)"
- )
+ assert (
+ SIDEBAR / "tab_context_menu.py"
+ ).is_file(), "tab_context_menu.py missing — #2662 may be phantom-closed"
+ assert (
+ SIDEBAR / "tab_context_menu.py"
+ ).stat().st_size > 500, "tab_context_menu.py appears to be a stub (< 500 bytes)"
@pytest.mark.unit
@@ -141,13 +141,13 @@ def test_2673_jupyter_tab_phased_implementation() -> None:
"""
# The phased implementation should have a jupyter_tab subpackage
jupyter_dir = SIDEBAR / "jupyter_tab"
- assert jupyter_dir.is_dir(), (
- "jupyter_tab/ directory missing — phased Jupyter implementation not landed"
- )
+ assert (
+ jupyter_dir.is_dir()
+ ), "jupyter_tab/ directory missing — phased Jupyter implementation not landed"
assert (jupyter_dir / "widget.py").is_file(), "jupyter_tab/widget.py missing"
- assert (jupyter_dir / "availability.py").is_file(), (
- "jupyter_tab/availability.py missing (soft-dependency guard)"
- )
+ assert (
+ jupyter_dir / "availability.py"
+ ).is_file(), "jupyter_tab/availability.py missing (soft-dependency guard)"
@pytest.mark.unit
@@ -182,17 +182,17 @@ def test_2675_shared_calculator_workspace_contract() -> None:
workspace_contract = (
REPO_ROOT / "src" / "shared" / "python" / "sidekick" / "workspace_contract.py"
)
- assert workspace_contract.is_file(), (
- "workspace_contract.py missing — #2675 shared contract not implemented"
- )
+ assert (
+ workspace_contract.is_file()
+ ), "workspace_contract.py missing — #2675 shared contract not implemented"
@pytest.mark.unit
def test_2676_host_integration() -> None:
"""#2676: Proven shared host integration across downstream consumers."""
- assert (INTEGRATION / "test_sidekick_host_integration.py").is_file(), (
- "Integration test file missing for #2676"
- )
+ assert (
+ INTEGRATION / "test_sidekick_host_integration.py"
+ ).is_file(), "Integration test file missing for #2676"
content = (INTEGRATION / "test_sidekick_host_integration.py").read_text(
encoding="utf-8"
)
@@ -251,9 +251,9 @@ def test_2682_symbolic_solver() -> None:
Being implemented on branch fix/issue-2934-symbolic-solver.
"""
- assert (SIDEKICK / "symbolic_engine.py").is_file(), (
- "symbolic_engine.py missing — #2682 not yet fully landed"
- )
+ assert (
+ SIDEKICK / "symbolic_engine.py"
+ ).is_file(), "symbolic_engine.py missing — #2682 not yet fully landed"
@pytest.mark.unit
@@ -276,9 +276,9 @@ def test_2684_rotation_converter_tab() -> None:
"""
assert (SIDEBAR / "default_tabs.py").is_file()
content = (SIDEBAR / "default_tabs.py").read_text(encoding="utf-8")
- assert "rotation" in content.lower() or "ROTATION_CONVERTER" in content, (
- "default_tabs.py does not appear to include Rotation Converter tab"
- )
+ assert (
+ "rotation" in content.lower() or "ROTATION_CONVERTER" in content
+ ), "default_tabs.py does not appear to include Rotation Converter tab"
@pytest.mark.unit
@@ -396,6 +396,6 @@ def test_epic_2661_implementation_summary() -> None:
UserWarning,
stacklevel=2,
)
- assert len(present) + len(missing_core) == len(files_to_check), (
- "Epic #2661 summary inventory lost or duplicated file entries"
- )
+ assert len(present) + len(missing_core) == len(
+ files_to_check
+ ), "Epic #2661 summary inventory lost or duplicated file entries"
diff --git a/tests/unit/test_sidekick_import_deprecation.py b/tests/unit/test_sidekick_import_deprecation.py
index 3711f79f23..d239e114e8 100644
--- a/tests/unit/test_sidekick_import_deprecation.py
+++ b/tests/unit/test_sidekick_import_deprecation.py
@@ -93,9 +93,9 @@ def test_sidekick_package_exists() -> None:
f"sidekick package directory missing: {SIDEKICK_SRC}. "
"The Phase 2 rename has not been executed."
)
- assert (SIDEKICK_SRC / "__init__.py").is_file(), (
- f"sidekick/__init__.py missing — package is incomplete: {SIDEKICK_SRC}"
- )
+ assert (
+ SIDEKICK_SRC / "__init__.py"
+ ).is_file(), f"sidekick/__init__.py missing — package is incomplete: {SIDEKICK_SRC}"
@pytest.mark.unit
@@ -105,7 +105,9 @@ def test_deprecation_shim_exists() -> None:
f"Deprecation shim directory missing: {SHIM_DIR}. "
"Create it with a DeprecationWarning on import."
)
- assert (SHIM_DIR / "__init__.py").is_file(), (
+ assert (
+ SHIM_DIR / "__init__.py"
+ ).is_file(), (
f"upstream_drift_tools/__init__.py missing — shim is not a package: {SHIM_DIR}"
)
@@ -251,6 +253,6 @@ def test_canonical_package_importable() -> None:
import sidekick # noqa: F401
assert sidekick is not None
- assert hasattr(sidekick, "__version__"), (
- "sidekick package must expose __version__ for downstream compatibility"
- )
+ assert hasattr(
+ sidekick, "__version__"
+ ), "sidekick package must expose __version__ for downstream compatibility"
diff --git a/tests/unit/test_sidekick_package_rename.py b/tests/unit/test_sidekick_package_rename.py
index 6ef10e71e4..224fbb68f4 100644
--- a/tests/unit/test_sidekick_package_rename.py
+++ b/tests/unit/test_sidekick_package_rename.py
@@ -56,21 +56,18 @@ def _assert_import_probe_succeeds(result: subprocess.CompletedProcess[str]) -> N
@pytest.mark.unit
def test_sidekick_package_importable() -> None:
"""The new canonical name must be importable."""
- result = _run_import_probe(
- """
+ result = _run_import_probe("""
import sidekick
assert sidekick is not None
- """
- )
+ """)
_assert_import_probe_succeeds(result)
@pytest.mark.unit
def test_upstream_drift_tools_shim_imports() -> None:
"""Old name still works (backward compat) and emits a DeprecationWarning."""
- result = _run_import_probe(
- """
+ result = _run_import_probe("""
import warnings
with warnings.catch_warnings(record=True) as caught:
@@ -87,16 +84,14 @@ def test_upstream_drift_tools_shim_imports() -> None:
"Expected at least one DeprecationWarning about 'deprecated' from "
f"the shim, but got: {[str(warning.message) for warning in caught]}"
)
- """
- )
+ """)
_assert_import_probe_succeeds(result)
@pytest.mark.unit
def test_shim_and_canonical_are_same_object() -> None:
"""Shim re-exports point to the same canonical sidekick objects (no duplication)."""
- result = _run_import_probe(
- """
+ result = _run_import_probe("""
import warnings
import sidekick.data_processing
@@ -109,8 +104,7 @@ def test_shim_and_canonical_are_same_object() -> None:
"sidekick.data_processing and upstream_drift_tools.data_processing "
"must be the same module object (shim must proxy, not copy)"
)
- """
- )
+ """)
_assert_import_probe_succeeds(result)