Skip to content

SimulationEngine infrastructure for 2D/3D modes#294

Merged
ekiefl merged 13 commits into
mainfrom
ek/motion-modules
May 17, 2026
Merged

SimulationEngine infrastructure for 2D/3D modes#294
ekiefl merged 13 commits into
mainfrom
ek/motion-modules

Conversation

@ekiefl
Copy link
Copy Markdown
Owner

@ekiefl ekiefl commented May 17, 2026

SimulationEngine infrastructure for 2D/3D modes

Summary

This is the first concrete step against the plan I'd settled on: stop trying to rebase the 3d branch, vendor pieces into main behind a flag instead. The flag's semantic boundary is whether the airborne motion state is reachable — if no model can produce vz, balls just never leave the table, BALL_TABLE events never fire, airborne branches are dead code. That collapses 2D vs 3D to one boolean on the engine that already wraps the resolver and gets passed to simulate().

This PR delivers the infrastructure for that flag. No 3D physics yet — but the slot is built, tagged, and validated.

What's in this PR

The flag itself (SimulationEngine.is_3d: bool = False):

  • Each Resolver model and EventDetector strategy declares a Dim capability — TWO, THREE, or BOTH.
  • Dim.BOTH is a strict promise: the strategy behaves the same for both (it doesn't know what mode the simulator is in). If a strategy would behave differently across modes, it's two strategies, not one Dim.BOTH.
  • SimulationEngine.__attrs_post_init__ validates that every bundled strategy is compatible with is_3d. Incompatible pairings raise with a message naming the offending strategy.

EventDetector as a peer to Resolver in pooltool/evolution/event_based/detect/:

  • One strategy class per event type (stick_ball, ball_ball, ball_linear_cushion, ball_circular_cushion, ball_pocket).
  • EventDetector.get_next_event() is the orchestration method. The old free-function get_next_event and get_next_* collection in simulate.py are deleted.
  • Detection now flows: _SimulationState.stepself.engine.detector.get_next_event(...).

Architectural moves falling out of the above:

  • PhysicsEngineSimulationEngine, moved from pooltool/physics/engine.py to pooltool/evolution/engine.py. It bundles detection now, so "physics" is no longer accurate.
  • solve.pypooltool/physics/motion/solve.py. Low-level numerics belong in physics, not in the event loop.
  • 10 ball-physics helpers (rel_velocity, get_slide_time, get_ball_energy, etc.) moved from pooltool/ptmath/utils.py to pooltool/physics/utils.py. They consume rvw arrays — they're physics, not pure math. This was already done in the 3d branch.
  • _system_has_energy lives in pooltool/evolution/event_based/_utils.py — the layer that depends on both system and physics.

What's NOT in this PR

  • Any Dim.THREE strategy implementations
  • BALL_TABLE event type or its detector/resolver
  • airborne motion state in pooltool/constants.py
  • 3D-aware cushion geometry (line/cylinder collision math)
  • Any vendoring from the 3d branch beyond architectural decisions

Each of those is to be delivered in follow-up PRs that lands independently against the infrastructure in this one.

Things refined vs. the original plan

A couple of design points evolved during implementation:

  • No default_2d() / default_3d() factories on SimulationEngine. I'd originally pitched these; rejected because Resolver is user-owned via ~/.config/pooltool/physics/resolver.yaml, and a factory would override user customization. Users pick resolver models independently; SimulationEngine just validates the bundle is internally consistent with its is_3d setting.
  • Per-motion-state ownership of collision-time math deferred. Considered unifying evolve/__init__.py and solve.py into per-state modules (sliding/rolling/spinning each owning both forward-step and collision-time math). Rejected for this PR — high-cost change with relatively weak payoff. solve.py was relocated into physics/motion/ to leave room for the unification later if desired.

Summary by CodeRabbit

  • New Features

    • Added SimulationEngine for pluggable strategy management with automatic dimensionality validation
    • Introduced event-based collision detection architecture with specialized detectors for ball-ball, ball-cushion, ball-pocket, and stick-ball interactions
    • Added dimensionality (Dim) system to declare physics strategy compatibility
  • Documentation

    • Updated example notebooks to demonstrate new SimulationEngine usage
    • Enhanced custom physics guide with additional attribute requirements
    • Updated developer guide references
  • Refactor

    • Reorganized physics utilities into dedicated module
    • Migrated simulation engine responsibilities to new architecture

Review Change Stack

@review-notebook-app
Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7e9770e4-6b78-4053-9039-6218f7e39e93

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR refactors pooltool's event-based simulation architecture from function-centric event handling to a strategy-based SimulationEngine with pluggable detectors and resolvers. It relocates physics utilities from ptmath to a new physics.utils module, introduces dimensionality compatibility validation, and updates all collision strategies to declare their dimensional support.

Changes

Core SimulationEngine and Physics Foundations

Layer / File(s) Summary
Dimensionality enum and physics utilities
pooltool/physics/dimensionality.py, pooltool/physics/utils.py
Introduces Dim enum (TWO, THREE, BOTH) for strategy compatibility declarations. Creates physics/utils.py with JIT-compiled helpers for surface velocity, relative velocity, time scales (slide/roll/spin), ball energy, and cue-tip geometry—all migrated from ptmath.utils.
SimulationEngine and detection dispatcher
pooltool/evolution/engine.py, pooltool/evolution/event_based/detect/detector.py, pooltool/evolution/__init__.py
Introduces SimulationEngine with resolver and detector fields; validates strategy dimensionality at construction. Adds EventDetector orchestrator that aggregates five detection strategy types, prioritizes simultaneous events by tier and energy, and returns the next collision/transition event.
Event detection strategies
pooltool/evolution/event_based/detect/{ball_ball,ball_cushion,ball_pocket,stick_ball}.py, pooltool/evolution/event_based/detect/__init__.py, pooltool/evolution/event_based/_utils.py
Implements five pluggable detection strategies (ball-ball, linear cushion, circular cushion, pocket, stick-ball) that cache collision times and compute next event for their type. Adds _system_has_energy helper to check if any ball has kinetic energy.

Physics Strategy Dimensionality Declarations

Layer / File(s) Summary
Ball-ball collision strategies with dim
pooltool/physics/resolve/ball_ball/{core,friction,frictional_inelastic,frictional_mathavan,frictionless_elastic}/*
Adds dim: Dim = Dim.TWO to FrictionalInelastic, FrictionalMathavan, FrictionlessElastic; updates BallBallCollisionStrategy protocol to require dim. Migrates friction computations to use physics.utils.tangent_surface_velocity.
Ball-cushion collision strategies with dim
pooltool/physics/resolve/ball_cushion/{core,han_2005,impulse_frictional_inelastic,mathavan_2010,stronge_compliant,unrealistic}/*
Adds dim: Dim = Dim.TWO to all linear and circular cushion collision models (Han2005, Mathavan2010, Stronge-compliant, Impulse-frictional, Unrealistic). Updates BallLCushionCollisionStrategy and BallCCushionCollisionStrategy protocols to require dim. Migrates surface velocity calls to physics.utils.
Pocket and transition strategies with dim
pooltool/physics/resolve/{ball_pocket,transition}/__init__.py
Adds dim: Dim = Dim.TWO to CanonicalBallPocket and CanonicalTransition; updates BallPocketStrategy and BallTransitionStrategy protocols to require dim.

Event-Based Simulation Refactor

Layer / File(s) Summary
Introspection snapshot engine swap
pooltool/evolution/event_based/introspection.py
Changes SimulationSnapshot.engine and SimulationSnapshotSequence.engine to store SimulationEngine instead of PhysicsEngine. Updates simulate_with_snapshots function signature to accept SimulationEngine | None.
Simulate module delegates to engine
pooltool/evolution/event_based/simulate.py
Refactors simulate() to delegate event detection to engine.detector.get_next_event() and resolution to engine.resolver.resolve(). Removes 404 lines of prior standalone functions (get_event_priority, get_next_event, and collision-specific helpers). Updates DEFAULT_ENGINE from PhysicsEngine() to SimulationEngine().

Physics Utilities Migration and Module Updates

Layer / File(s) Summary
Import migrations and utility re-routing
pooltool/physics/{__init__,evolve/__init__,motion/solve}.py, pooltool/ani/{hud,modes/{aim,view}}.py, pooltool/objects/cue/render.py
Updates imports across animation, cue render, and physics modules to source velocity/time/energy helpers from pooltool.physics.utils instead of ptmath. Removes PhysicsEngine from pooltool/physics/__init__.py and adds re-exports for the new utility functions.
ptmath deprecation and cleanup
pooltool/ptmath/{__init__,utils}.py, pooltool/physics/engine.py
Removes surface_velocity, rel_velocity, get_slide_time, get_roll_time, get_spin_time, get_ball_energy, tip_contact_offset, tip_center_offset from ptmath/utils.py; removes those names from ptmath.__all__ exports. Adds new is_overlapping helper to ptmath. Deletes PhysicsEngine class and module from pooltool/physics/engine.py.

Tests and Documentation

Layer / File(s) Summary
Event-based simulation and engine tests
tests/evolution/{event_based/test_simulate.py,test_engine.py}
Refactors test_simulate.py to use EventDetector().get_next_event() instead of module functions. Adds new test_engine.py with validation tests for dimensionality compatibility, strategy incompatibility detection, and BOTH dim acceptance.
Physics tests updated
tests/physics/{test_utils.py,resolve/ball_ball/test_*.py,resolve/ball_cushion/test_*.py}
Updates physics tests to import and use pooltool.physics.utils helpers (velocity, energy) instead of ptmath versions.
Documentation and examples
docs/{examples/30_degree_rule.ipynb,include_exclude.py,meta/developer_guide.md,resources/custom_physics.md}, pooltool/objects/table/components.py
Updates notebook example to use SimulationEngine() instead of PhysicsEngine(). Updates developer guide and custom physics documentation to reference SimulationEngine and Dim enum. Updates Sphinx doc exclusion to skip pooltool.evolution.engine. Updates docstrings to reference detection behavior instead of specific function names.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ekiefl/pooltool#232: Introduces event-based introspection APIs that the main PR refactors to use SimulationEngine instead of PhysicsEngine.
  • ekiefl/pooltool#240: Modifies Stronge-compliant ball-cushion models that the main PR updates with surface velocity helpers and dim attributes.
  • ekiefl/pooltool#256: Modifies Stronge-compliant cushion collision logic (_solve/solve methods) that overlaps with the main PR's surface velocity migration.

Suggested reviewers

  • derek-mcblane
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main structural change: introduction of SimulationEngine infrastructure with 2D/3D mode support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ek/motion-modules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ekiefl
Copy link
Copy Markdown
Owner Author

ekiefl commented May 17, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 88.80407% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.45%. Comparing base (b2ce363) to head (5ffb875).

Files with missing lines Patch % Lines
pooltool/physics/utils.py 51.92% 25 Missing ⚠️
pooltool/evolution/event_based/detect/detector.py 93.15% 5 Missing ⚠️
pooltool/physics/evolve/__init__.py 20.00% 4 Missing ⚠️
...resolve/ball_ball/frictional_inelastic/__init__.py 42.85% 4 Missing ⚠️
pooltool/evolution/event_based/cache.py 80.00% 1 Missing ⚠️
pooltool/evolution/event_based/detect/ball_ball.py 97.43% 1 Missing ⚠️
...ltool/evolution/event_based/detect/ball_cushion.py 98.21% 1 Missing ⚠️
...ooltool/evolution/event_based/detect/stick_ball.py 95.65% 1 Missing ⚠️
pooltool/physics/motion/solve.py 50.00% 1 Missing ⚠️
...ool/physics/resolve/sphere_half_space_collision.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #294      +/-   ##
==========================================
+ Coverage   46.36%   47.45%   +1.09%     
==========================================
  Files         144      153       +9     
  Lines       10314    10479     +165     
==========================================
+ Hits         4782     4973     +191     
+ Misses       5532     5506      -26     
Flag Coverage Δ
service 47.45% <88.80%> (+1.09%) ⬆️
service-no-ani 58.01% <88.65%> (+1.49%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pooltool/physics/utils.py (1)

128-151: ⚡ Quick win

Rename the inverse-transform input to match semantics.

Line 129 and Line 141 describe different inputs; this helper should take a contact-point offset and return cue-center offset. Aligning the parameter/doc names will make call sites less error-prone.

♻️ Proposed fix
 `@jit`(nopython=True, cache=const.use_numba_cache)
 def tip_center_offset(
-    tip_center_offset: NDArray[np.float64], tip_radius: float, ball_radius: float
+    contact_point_offset: NDArray[np.float64], tip_radius: float, ball_radius: float
 ) -> NDArray[np.float64]:
@@
-    Args:
-        cue_center_offset:
-            A 2D vector (e.g., [a, b]) representing the offset of the cue tip center
-            relative to the ball center (normalized by the ball's radius).
+    Args:
+        contact_point_offset:
+            A 2D vector (e.g., [a, b]) representing the contact point offset on the
+            ball's surface (normalized by the ball's radius).
@@
-    return tip_center_offset * (1 + tip_radius / ball_radius)
+    return contact_point_offset * (1 + tip_radius / ball_radius)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pooltool/physics/utils.py` around lines 128 - 151, The function
tip_center_offset currently names its input tip_center_offset but actually
expects a contact-point offset and returns the cue tip center offset; rename the
input parameter to reflect this (e.g., contact_offset or tip_contact_offset) and
update the docstring Arg name and description to match, keeping the computation
tip_center = contact_offset * (1 + tip_radius/ball_radius) and the function name
tip_center_offset unchanged so callers remain clear; ensure the return docstring
describes the cue tip center offset (normalized) and update any internal
references to the old parameter name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/resources/custom_physics.md`:
- Line 202: Update the broken markdown anchor for the SimulationEngine reference
used in the Dim.TWO line: replace the old anchor target
"`#pooltool.evolution.engine.SimulationEngine`" with the exported API path anchor
"`#pooltool.evolution.SimulationEngine`" (so the link from the Dim.TWO description
points to pooltool.evolution.SimulationEngine). Locate the text mentioning
Dim.TWO and adjust the link target accordingly to the exported SimulationEngine
symbol.

In `@pooltool/evolution/engine.py`:
- Around line 43-51: The current code bypasses dimensionality checks for
non-attrs strategies by using attrs.has(type(strategy)); remove that attrs.has
gate so validation always runs: in the block referencing strategy, bundle, field
and Dim, first ensure the strategy exposes a dim attribute (keep the
hasattr(strategy, "dim") check and its AttributeError using bundle and field),
then validate that strategy.dim is in (required, Dim.BOTH) and raise the same
error if not; do not rely on attrs.has(type(strategy)) so custom non-attrs
strategies can't skip the dim checks.

---

Nitpick comments:
In `@pooltool/physics/utils.py`:
- Around line 128-151: The function tip_center_offset currently names its input
tip_center_offset but actually expects a contact-point offset and returns the
cue tip center offset; rename the input parameter to reflect this (e.g.,
contact_offset or tip_contact_offset) and update the docstring Arg name and
description to match, keeping the computation tip_center = contact_offset * (1 +
tip_radius/ball_radius) and the function name tip_center_offset unchanged so
callers remain clear; ensure the return docstring describes the cue tip center
offset (normalized) and update any internal references to the old parameter
name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1614b63d-681c-4b55-bae3-92a9dcb9a6b8

📥 Commits

Reviewing files that changed from the base of the PR and between b2ce363 and a2f83f2.

📒 Files selected for processing (52)
  • docs/examples/30_degree_rule.ipynb
  • docs/include_exclude.py
  • docs/meta/developer_guide.md
  • docs/resources/custom_physics.md
  • pooltool/ani/hud.py
  • pooltool/ani/modes/aim.py
  • pooltool/ani/modes/view.py
  • pooltool/evolution/__init__.py
  • pooltool/evolution/engine.py
  • pooltool/evolution/event_based/_utils.py
  • pooltool/evolution/event_based/cache.py
  • pooltool/evolution/event_based/detect/__init__.py
  • pooltool/evolution/event_based/detect/ball_ball.py
  • pooltool/evolution/event_based/detect/ball_cushion.py
  • pooltool/evolution/event_based/detect/ball_pocket.py
  • pooltool/evolution/event_based/detect/detector.py
  • pooltool/evolution/event_based/detect/stick_ball.py
  • pooltool/evolution/event_based/introspection.py
  • pooltool/evolution/event_based/simulate.py
  • pooltool/objects/cue/render.py
  • pooltool/objects/table/components.py
  • pooltool/physics/__init__.py
  • pooltool/physics/dimensionality.py
  • pooltool/physics/engine.py
  • pooltool/physics/evolve/__init__.py
  • pooltool/physics/motion/__init__.py
  • pooltool/physics/motion/solve.py
  • pooltool/physics/resolve/ball_ball/core.py
  • pooltool/physics/resolve/ball_ball/friction.py
  • pooltool/physics/resolve/ball_ball/frictional_inelastic/__init__.py
  • pooltool/physics/resolve/ball_ball/frictional_mathavan/__init__.py
  • pooltool/physics/resolve/ball_ball/frictionless_elastic/__init__.py
  • pooltool/physics/resolve/ball_cushion/core.py
  • pooltool/physics/resolve/ball_cushion/han_2005/model.py
  • pooltool/physics/resolve/ball_cushion/impulse_frictional_inelastic/model.py
  • pooltool/physics/resolve/ball_cushion/mathavan_2010/model.py
  • pooltool/physics/resolve/ball_cushion/stronge_compliant/model.py
  • pooltool/physics/resolve/ball_cushion/unrealistic/__init__.py
  • pooltool/physics/resolve/ball_pocket/__init__.py
  • pooltool/physics/resolve/sphere_half_space_collision.py
  • pooltool/physics/resolve/stick_ball/core.py
  • pooltool/physics/resolve/stick_ball/instantaneous_point/__init__.py
  • pooltool/physics/resolve/transition/__init__.py
  • pooltool/physics/utils.py
  • pooltool/ptmath/__init__.py
  • pooltool/ptmath/utils.py
  • tests/evolution/event_based/test_simulate.py
  • tests/evolution/test_engine.py
  • tests/physics/resolve/ball_ball/test_ball_ball.py
  • tests/physics/resolve/ball_ball/test_frictional_mathavan.py
  • tests/physics/resolve/ball_cushion/test_ball_cushion.py
  • tests/physics/test_utils.py
💤 Files with no reviewable changes (3)
  • pooltool/physics/engine.py
  • pooltool/ptmath/init.py
  • pooltool/ptmath/utils.py


`Dim` is a capability declaration consumed at engine construction:

- `Dim.TWO` — your model is safe only when [](#pooltool.evolution.engine.SimulationEngine)'s `is_3d` is `False`. Use this if your model assumes balls are on the table surface (z=R, vz=0).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix broken SimulationEngine cross-reference target.

This anchor points to pooltool.evolution.engine.SimulationEngine, but that module is now excluded from AutoAPI. Link to the exported API path (pooltool.evolution.SimulationEngine) instead.

🔧 Suggested patch
-- `Dim.TWO` — your model is safe only when [](`#pooltool.evolution.engine.SimulationEngine`)'s `is_3d` is `False`. Use this if your model assumes balls are on the table surface (z=R, vz=0).
+- `Dim.TWO` — your model is safe only when [](`#pooltool.evolution.SimulationEngine`)'s `is_3d` is `False`. Use this if your model assumes balls are on the table surface (z=R, vz=0).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- `Dim.TWO` — your model is safe only when [](#pooltool.evolution.engine.SimulationEngine)'s `is_3d` is `False`. Use this if your model assumes balls are on the table surface (z=R, vz=0).
- `Dim.TWO` — your model is safe only when [](`#pooltool.evolution.SimulationEngine`)'s `is_3d` is `False`. Use this if your model assumes balls are on the table surface (z=R, vz=0).
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 202-202: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/resources/custom_physics.md` at line 202, Update the broken markdown
anchor for the SimulationEngine reference used in the Dim.TWO line: replace the
old anchor target "`#pooltool.evolution.engine.SimulationEngine`" with the
exported API path anchor "`#pooltool.evolution.SimulationEngine`" (so the link
from the Dim.TWO description points to pooltool.evolution.SimulationEngine).
Locate the text mentioning Dim.TWO and adjust the link target accordingly to the
exported SimulationEngine symbol.

Comment on lines +43 to +51
if not attrs.has(type(strategy)):
continue
if not hasattr(strategy, "dim"):
raise AttributeError(
f"{type(bundle).__name__}.{field.name} "
f"({type(strategy).__name__}) is missing required "
f"'dim' attribute"
)
if strategy.dim not in (required, Dim.BOTH):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dimensionality validation is bypassable for non-attrs strategy implementations.

Line 43 skips validation when a strategy instance is not an attrs class. That allows incompatible custom strategies to pass without dim checks, despite the engine contract.

Suggested fix
-                if not attrs.has(type(strategy)):
-                    continue
                 if not hasattr(strategy, "dim"):
                     raise AttributeError(
                         f"{type(bundle).__name__}.{field.name} "
                         f"({type(strategy).__name__}) is missing required "
                         f"'dim' attribute"
                     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not attrs.has(type(strategy)):
continue
if not hasattr(strategy, "dim"):
raise AttributeError(
f"{type(bundle).__name__}.{field.name} "
f"({type(strategy).__name__}) is missing required "
f"'dim' attribute"
)
if strategy.dim not in (required, Dim.BOTH):
if not hasattr(strategy, "dim"):
raise AttributeError(
f"{type(bundle).__name__}.{field.name} "
f"({type(strategy).__name__}) is missing required "
f"'dim' attribute"
)
if strategy.dim not in (required, Dim.BOTH):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pooltool/evolution/engine.py` around lines 43 - 51, The current code bypasses
dimensionality checks for non-attrs strategies by using
attrs.has(type(strategy)); remove that attrs.has gate so validation always runs:
in the block referencing strategy, bundle, field and Dim, first ensure the
strategy exposes a dim attribute (keep the hasattr(strategy, "dim") check and
its AttributeError using bundle and field), then validate that strategy.dim is
in (required, Dim.BOTH) and raise the same error if not; do not rely on
attrs.has(type(strategy)) so custom non-attrs strategies can't skip the dim
checks.

@ekiefl ekiefl merged commit eb128f1 into main May 17, 2026
12 checks passed
@ekiefl ekiefl deleted the ek/motion-modules branch May 17, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant