-
Notifications
You must be signed in to change notification settings - Fork 0
Vectorisation bis #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vectorisation bis #201
Conversation
Change all 0:grid_size indexing to 1:grid_size+1 in the exa backend: - State and control variable creation ranges - Dynamics constraint loops (0:grid_size-1 → 1:grid_size) - Lagrange cost integration loops with proper scheme handling - Path constraint loops (0:grid_size → 1:grid_size+1) - Initial/final condition indexing (0/grid_size → 1/grid_size+1) - Mayer cost boundary state indexing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace :grid_size+1 with :(grid_size+1) for proper Julia expression quoting in final state index arguments: - Boundary constraints (subs2 call) - Final constraints (subs3 call) - Mayer cost (subs2 call) Keeps range expressions like 1:grid_size+1 unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This migration enables standard matrix operations (range slicing, arithmetic, broadcasting) on state variables in the exa backend by using p.x_m (a matrix of ExaModels.Var) instead of p.x (ExaModels.Variable). Changes: - Add x_m field to ParsingInfo struct - Initialize p.x_m in p_state! and p_state_exa! functions - Replace p.x with p.x_m in 13 locations across 4 functions: * p_constraint_exa!: boundary and path constraints (5 replacements) * p_dynamics_coord_exa!: state dynamics discretization (4 replacements) * p_lagrange_exa!: Lagrange cost function (2 replacements) * p_mayer_exa!: Mayer cost function (2 replacements) - Remove 3 useless substitution lines in p_constraint_exa! - Enable direct arithmetic operations: p.x_m[i, j+1] - p.x_m[i, j] The p.x_m matrix is defined as [x[i, j] for i ∈ 1:n, j ∈ 1:grid_size+1], providing more efficient access patterns for discretized optimal control. Related to issue #181 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Ignore Claude context files used for development documentation that should not be tracked in version control. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit addresses the p.x_m indexing bug and standardizes on 0-based
grid indexing for the ExaModels backend.
Changes:
1. Removed p.x_m matrix wrapper from ParsingInfo struct
2. Reverted all p.x_m references back to p.x (ExaModels.Variable)
- p.x properly handles symbolic indexing in ExaModels generators
- p.x_m (plain Matrix) was causing ArgumentError with symbolic indices
3. Migrated from 1-based to 0-based grid indexing:
- State/control variables: 0:grid_size (was 1:grid_size+1)
- Initial conditions: index 0 (was 1)
- Final conditions: index grid_size (was grid_size+1)
- Dynamics constraints: 0:grid_size-1 (was 1:grid_size)
- Path constraints: 0:grid_size (was 1:grid_size+1)
- Lagrange cost integration:
* Euler forward: 0:grid_size-1 (was 1:grid_size)
* Euler implicit: 1:grid_size (was 2:grid_size+1)
* Midpoint: 0:grid_size-1 (was 1:grid_size)
* Trapeze: endpoints (0, grid_size), interior 1:grid_size-1
4. Documentation improvements in utils.jl for subs3 and subs4
Rationale:
The p.x_m approach attempted to enable matrix operations but failed because
generator variables in ExaModels become symbolic types (ParSource, Node2),
which cannot index regular Julia arrays. Using p.x (ExaModels.Variable)
throughout ensures proper symbolic indexing support.
0-based indexing provides cleaner semantics for discretization schemes,
with grid points at times t0 + j*dt for j in 0:grid_size.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… (min/max) as these two things are known statically (= ater parsing, without evaluating), plus the fact p.criterion is a symbol (:min...)
Relocated these utility functions to utils.jl for better organization: - is_range(x): Predicate to test if x is a range (AbstractRange or i:j expr) - as_range(x): Normalizes x to a range or single-element array Also added clarifying comments at all as_range call sites explaining the "case rg = i (vs i:j or i:p:j)" normalization. These functions are general-purpose utilities used for constraint index handling and belong with other utility functions rather than in the parsing logic.
Major enhancements:
1. Enhanced subs2 function (utils.jl):
- Added range indexing support: x[1:3] → [y[k, j] for k ∈ 1:3]
- Preserves scalar indexing behavior: x[i] → y[i, j]
- Added optional 'k' parameter for predictable symbol generation in tests
- New pattern: :($xx[$rg]) when is_range(rg) generates comprehension
- Existing pattern: :($xx[$i]) continues to work for scalars
2. Reorganized utility functions (onepass.jl → utils.jl):
- Moved __symgen() to utils.jl (needed by subs2 for range support)
- Consolidated is_range() and as_range() at top of utils.jl
- Better organization: all utility functions now in one place
3. Comprehensive test suite (test_utils.jl):
- 18 tests for subs2 (up from 2 original tests)
- Tests organized into 6 testsets:
* Scalar indexing (backward compatibility - 4 tests)
* Range indexing (new functionality - 5 tests)
* Mixed scalar/range (1 test)
* Nested/complex expressions (3 tests)
* Edge cases (4 tests)
* Backward compatibility verification (1 test)
- All tests use explicit k parameter for exact equality assertions
4. Test configuration (runtests.jl):
- Default to running only utils and utils_bis tests
- Other tests disabled to speed up development iteration
- Can still run all tests with: julia test/runtests.jl all
Implementation details:
- Range detection uses is_range() predicate (checks AbstractRange or i:j expr)
- Comprehension variable k generated via __symgen() or passed explicitly
- Backward compatible: existing code using scalar indices unchanged
- Forward compatible: supports symbolic ranges (1:n, 2:2:grid_size, etc.)
This enhancement enables tensor/matrix operations in the ExaModels backend
by allowing range-based substitutions in symbolic expressions.
Re-enabled all tests (aqua, prefix, onepass_fun, onepass_exa) that were temporarily disabled during subs2 development and testing.
This commit completes the migration to the enhanced subs2 function that now
handles three patterns: scalar indexing, range indexing, and bare symbols.
Changes:
- src/onepass.jl: Updated all 14 subs2 calls to include the new dim parameter
- State substitutions use p.dim_x
- Control substitutions use p.dim_u
- Locations: p_constraint_exa! (lines 700-801), p_dynamics_coord_exa! (lines 900-907),
p_lagrange_exa! (lines 970-974), p_mayer_exa! (lines 1033-1034)
- test/test_utils.jl: Updated all subs2 test calls to include dim parameter
- Modified test expectations for bare symbol behavior (now expands to comprehensions)
- Added explicit k parameter where needed for predictable test results
- Updated backward compatibility tests to reflect new bare symbol pattern
- src/utils.jl: Enhanced subs2 docstring
- Documents all three substitution patterns
- Explains the dim parameter for bare symbol expansion
- Provides comprehensive examples for scalar, range, and bare symbol usage
The new subs2 pattern `x` (bare symbol) → `[y[k, j] for k ∈ 1:dim]` enables
tensor operations where entire state/control vectors are referenced without
explicit indexing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit reverts the dim parameter addition to subs2, as bare symbol expansion (x → [y[k, j] for k ∈ 1:dim]) cannot be handled at the subs2 level. Changes: - src/onepass.jl: Removed dim parameter from all 14 subs2 calls - Lines 700-701: Boundary constraints (p_constraint_exa!) - Lines 800-801: Path constraints (p_constraint_exa!) - Lines 900-907: Dynamics discretization (p_dynamics_coord_exa!) - Lines 970-974: Lagrange cost (p_lagrange_exa!) - Lines 1033-1034: Mayer cost (p_mayer_exa!) - src/utils.jl: Updated subs2 docstring - Removed bare symbol pattern from documentation - Changed from 3 patterns to 2 patterns (scalar and range only) - Updated examples to show bare symbols are NOT substituted - Restored original signature: subs2(e, x, y, j; k = __symgen(:k)) - test/test_utils.jl: Updated all subs2 tests - Removed dim parameter from all 18 test calls - Updated Test 2 and Test 19 expectations (bare symbols NOT substituted) - Verified all range indexing tests still pass without dim parameter Rationale: Bare symbol expansion requires dimension information that is not available at the expression substitution level. This functionality will need to be implemented at a higher level in the parsing pipeline. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements the pattern from p_constraint_exa! boundary case across all functions that use subs2, enabling bare symbol handling (e.g., x(t) → [x[k, j] for k ∈ 1:dim_x]). Changes to src/onepass.jl: 1. p_mayer_exa! (lines 1030-1044): +3 lines - Added k = __symgen(:k) - Added subs for x0 and xf bare symbols 2. p_constraint_exa! path constraints (lines 797-815): +3 lines - Added k = __symgen(:k) - Added subs for xt and ut bare symbols 3. p_lagrange_exa! (lines 968-985): +4 lines - Added k = __symgen(:k) - Added subs for xt and ut bare symbols in ej1 and ej12 4. p_dynamics_coord_exa! (lines 900-920): +6 lines - Added k = __symgen(:k) - Added subs for xt and ut bare symbols in ej1, ej2, and ej12 Total: 16 lines added across 4 functions Pattern applied: - subs2 handles indexed cases: x[i] → y[i, j] and x[1:3] → [y[k, j] for k ∈ 1:3] - subs handles bare symbols: x → [y[k, j] for k ∈ 1:dim] - Same symbol k used for both state and control (sequential application) This completes the implementation of tensor support for bare symbols in the ExaModels backend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added 46 tests across 4 testsets to validate new bare symbol and range expression capabilities introduced by recent subs2 enhancements: 1. Bare symbols and ranges - costs (11 tests): - Lagrange costs with sum(x(t)), sum(x[1:2](t)), sum(u(t)) - Mayer costs at t0 and tf with bare symbols and ranges - Bolza costs combining both Mayer and Lagrange terms 2. Bare symbols and ranges - constraints (13 tests): - Initial constraints: sum(x(0)), x[1:2](0) - Final constraints: sum(x(tf)), x[2:3](tf) - Boundary constraints combining t0 and tf - Path constraints for state, control, and mixed - All constraint types with bare symbols and ranges 3. Bare symbols and ranges - dynamics (6 tests): - Dynamics using sum(x(t)), sum(x[2:3](t)) - Dynamics using sum(u(t)), sum(u[1:2](t)) - Mixed dynamics expressions 4. User-defined functions with ranges (10 tests): - Custom functions: f(x,u), g(x), h(u) - Applied in costs (Lagrange, Mayer, Bolza) - Applied in constraints (initial, final, path, mixed) - Applied in dynamics All tests run across all 4 schemes (euler, midpoint, trapeze, euler_implicit) and verify successful ExaModel creation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… symbol support for midpoint This commit includes several related improvements to the substitution functions and their usage in the exa backend: 1. Fixed critical bug in onepass.jl (lines 704, 1053): - Changed :grid_size (quoted symbol) to grid_size (unquoted variable) - This fixes MethodError when using bare symbols in Mayer costs and boundary constraints - Error was: "MethodError: no method matching -(::Symbol, ::Int64)" 2. Renamed subs5 → subs2m throughout codebase: - src/utils.jl: Function rename with updated docstring - src/onepass.jl: 2 call sites updated - test/test_utils.jl: Testset and 8 test calls updated - test/test_utils_bis.jl: Testset name and 1 test updated - test/runtests.jl: Import statement updated - Name now reflects it's the midpoint variant of subs2 3. Enhanced subs2m (formerly subs5): - Added k kwarg: function subs2m(e, x, y, j; k = __symgen(:k)) - Updated docstring to document both scalar and range indexing - Added example showing range substitution: x0[1:3] - Clarified that bare symbols are NOT substituted 4. Added comprehensive tests for subs2m in test/test_utils.jl: - 6 tests for range indexing (basic, step, arithmetic, multiple ranges, symbolic j, single-element) - 2 tests for backward compatibility (scalar indexing, bare symbols) - Total: 8 tests verifying all functionality 5. Added bare symbol handling after subs2m calls in onepass.jl: - Line 918 (p_dynamics_coord_exa!): Added subs for xt in midpoint dynamics - Line 990 (p_lagrange_exa!): Added subs for xt in midpoint Lagrange cost - Pattern: subs(ej12, xt, :([((x[k, j1] + x[k, j1 + 1]) / 2) for k ∈ 1:dim_x])) - Ensures bare symbols like x(t) expand correctly in midpoint scheme 6. Removed unused subs4 function: - Deleted function and docstring from src/utils.jl - Removed testset from test/test_utils.jl - Removed test from test/test_utils_bis.jl - Updated import in test/runtests.jl Files changed: - src/onepass.jl: 2 grid_size fixes, 2 subs5→subs2m renames, 2 bare symbol subs additions - src/utils.jl: subs2m rename/enhancement, subs4 removal, cross-reference updates - test/test_utils.jl: subs2m testset with 8 tests, subs4 testset removal - test/test_utils_bis.jl: Testset name update, subs4 test removal - test/runtests.jl: Import statement cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…Mayer terms Added parentheses around Mayer cost terms in Bolza cost expressions to ensure correct parsing. Without parentheses, the parser fails to distinguish the Mayer part from the Lagrange part. Changes: - Line 223: (sum(x(0))^2 + sum(x(1))^2) + ∫(sum(u(t))^2) → min - Line 236: (sum(x[1:2](0)) + sum(x[2:3](1))) + ∫(sum(u[1:2](t))) → min - Line 550: (f(x(0), [0, 0]) + f(x(1), [0, 0])) + ∫(h(u(t))) → min - Line 633: (f(x(0), [0, 0])) + ∫(f(x(t), u(t))) → min All 4 Bolza cost tests now have proper syntax: (Mayer terms) + ∫(Lagrange term) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…alars-for-exa scalarised constraint bounds
…oolbox/CTParser.jl into 181-dev-adding-tensors
- Add comprehensive tests for :other constraint type errors in test_onepass_fun_bis.jl and test_onepass_exa_bis.jl * Unit tests for p_constraint_fun!/p_constraint_exa! with :other type * Integration tests using @def and @def_exa macros for invalid constraints * Tests cover mixed state/control at different times (e.g., x(0)*u(t)+u(t)^2 <= 1) - Fix incorrect superscript exponents in test_onepass_fun.jl * Replace r²(__s) with (r^2)(__s) in lines 760 and 854 * Ensures proper aliasing for exponentiated variables - Update as_range tests in test_onepass_exa_bis.jl * Reflect new implementation returning :(($x):($x)) instead of [x] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Correct test from :((:foo):(:foo)) to :(foo:foo) - The implementation :(($x):($x)) interpolates x directly, producing :(foo:foo) - The previous test with nested quotes was incorrect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Change expected return values from [x] to :(($x):($x)) format - Line 95: [1] → :((1):(1)) - Line 97: [:x] → :(x:x) - Line 98: [:(x + 1)] → :((x + 1):(x + 1)) - Matches the updated as_range implementation in onepass.jl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Line 1800: Correct two bugs: - Add missing ∂ operator: (x₂)(t) → ∂(x₂)(t) - Add missing (t) arguments: x → x(t), u → u(t) Without ∂, the line was defining a constraint instead of dynamics. The vectorised version should match the non-vectorised dynamics. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
@ocots please review. some bug fixes + partial vectorisation for exa (check use case no. 8 below) CTParser.jl/test/test_onepass_exa.jl Line 1845 in bcd782a
|
📋 COMPREHENSIVE CODE REVIEW REPORTPR #201: Vectorisation bisDate: 2025-12-31 🎯 Executive SummaryPR Status: ✅ APPROVED WITH MINOR RECOMMENDATIONS Overall Assessment: This is a well-executed bug-fix and test-enhancement PR that successfully addresses GPU compatibility issues blocking the larger tensor implementation work (PR #182). The code demonstrates strong adherence to design principles, with excellent test coverage and careful refactoring. Key Strengths:
Recommendations:
📊 PR Metrics
🎯 PR Objective AnalysisLinked Issues and PRs
UnderstandingThis PR is a safe intermediate step to address GPU compatibility issues encountered in PR #182 (the main tensor implementation work). It fixes several minor but critical bugs that were blocking the tensor development:
The PR description states: "safe PR to deal with current issue on GPU with pr-adding-tensors #182 following exa_linalg addition" Key Requirements
Expected Changes
Scope AssessmentAppropriate ✅ This is a focused bug-fix and test-enhancement PR that:
🔍 Technical AnalysisCI/CD Status: ✅ ALL PASSINGResult: 13/13 checks passing, including GPU tests on moonshot ✅ 📁 Files Changed AnalysisCore Source Files1.
|
| File | Lines Added | Purpose |
|---|---|---|
test/test_onepass_fun_bis.jl |
+125 | Tests for :other constraint type errors |
test/test_onepass_exa_bis.jl |
+139 | Tests for as_range and vectorization |
test/test_onepass_exa.jl |
+883 | Comprehensive ExaModels backend tests |
test/test_utils.jl |
+160 | Tests for subs2, subs2m, as_range |
Total New Test Lines: +1,307 lines
Test Quality: 10/10 ✅
Strengths:
- ✅ Tests for error conditions (
:otherconstraint type) - ✅ Tests for edge cases (single-element ranges, bare symbols)
- ✅ Integration tests using
@defand@def_examacros - ✅ Tests for all substitution functions with multiple examples
- ✅ Tests for both
:funand:exabackends - ✅ Regression tests for previously fixed bugs
Example of Comprehensive Testing:
# Tests for :other constraint type (should error)
@testset ":other constraint type errors" begin
@test_throws ParsingError @def begin
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
x(0)*u(t) + u(t)^2 ≤ 1 # Mixed state/control at different times
∂(x)(t) == [u(t), 0]
end
endTest Coverage by Category
-
Unit Tests: ✅ Excellent
is_rangefunction with various input typesas_rangenormalizationsubs2,subs2m,subs3substitution functions
-
Integration Tests: ✅ Excellent
- Full problem definitions with
@defand@def_exa - Vectorized dynamics and constraints
- All constraint types (boundary, initial, final, path, mixed)
- Full problem definitions with
-
Error Handling Tests: ✅ Excellent
- Invalid constraint types
- Dimension mismatches
- Mixed state/control at different times
-
Regression Tests: ✅ Excellent
- Tests for previously fixed bugs (superscript exponents)
- Tests for
as_rangebehavior changes
📝 CODE QUALITY OBSERVATIONS
Positive Observations ✅
-
Excellent Commit Messages: Each commit has a clear, descriptive message explaining the change
- Example: "Fix incorrect as_range test expression" with detailed explanation in commit body
-
Consistent Coding Style: Follows Julia conventions and project patterns
- Proper use of
@matchfor pattern matching - Consistent naming with
__prefix for internal functions - Proper use of docstrings with
$(TYPEDSIGNATURES)
- Proper use of
-
Good Use of Julia Features:
- Pattern matching with
@match - Multiple dispatch
- Symbolic expression manipulation
- Generated code with proper quoting
- Type parameters and abstract types
- Pattern matching with
-
Defensive Programming: Runtime checks for dimension mismatches
code = :(length($e1) == length($e3) == length($rg) || throw("wrong bound dimension"))
-
Clear Documentation: Docstrings with examples for all new functions
- All new functions have comprehensive docstrings
- Examples demonstrate both typical and edge case usage
-
Incremental Development: 12 well-structured commits showing clear progression
- Each commit addresses a specific issue
- Commits are atomic and reversible
- Clear progression from bug fixes to test additions
Areas for Minor Improvement 🟡
1. Inline Comments for Complex Logic
Current (could be clearer):
e2 = subs2(e2, xt, p.x, j)
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))Suggested:
# First substitute scalar indices: x[i] → x[i, j]
e2 = subs2(e2, xt, p.x, j)
# Then handle bare symbols: x → [x[k, j] for k ∈ 1:n]
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))2. Extract Repeated Vectorization Pattern
Current (repeated in multiple places):
k = __symgen(:k)
e2 = subs2(e2, xt, p.x, j)
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
e2 = subs2(e2, ut, p.u, j)
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))Suggested (extract to helper function):
function vectorize_state_control(e, xt, ut, p, j)
k = __symgen(:k)
e = subs2(e, xt, p.x, j)
e = subs(e, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
e = subs2(e, ut, p.u, j)
e = subs(e, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))
return e
end3. More Specific Error Messages
Current:
_ => return __throw("bad constraint declaration", p.lnum, p.line)Suggested:
_ => return __throw("unsupported constraint type: $c_type (expected :boundary, :initial, :final, :state_fun, :control_fun, or :mixed)", p.lnum, p.line)4. Consider Adding Type Annotations
For performance-critical functions, consider adding type annotations:
# Current:
is_range(x) = false
# Suggested:
is_range(x)::Bool = false
is_range(x::T)::Bool where {T<:AbstractRange} = true
is_range(x::Expr)::Bool = (x.head == :call) && (x.args[1] == :(:))🎯 FINAL ASSESSMENT
Overall Scores
| Category | Score | Status |
|---|---|---|
| SOLID Principles | 9.2/10 | ✅ Excellent |
| Other Design Principles | 9.2/10 | ✅ Excellent |
| Reusability | 9/10 | ✅ Excellent |
| Performance | 9/10 | ✅ Excellent |
| Maintainability | 10/10 | ✅ Outstanding |
| Safety | 9/10 | ✅ Excellent |
| Test Coverage | 10/10 | ✅ Outstanding |
| Bug Fixes | 10/10 | ✅ Critical fixes |
| Documentation | 10/10 | ✅ Outstanding |
| Code Quality | 9/10 | ✅ Excellent |
Overall Score: 9.3/10 ✅
🚀 RECOMMENDATION
✅ APPROVED FOR MERGE
This PR is ready to merge with the following justifications:
Critical Success Factors
- ✅ All CI/CD checks passing (13/13 including GPU tests on moonshot)
- ✅ Critical bugs fixed
:control_funsymbol error (would break GPU compatibility)- Missing vectorization in constraints (essential for ExaModels)
- Inconsistent range handling (improves maintainability)
- ✅ Excellent test coverage (+1,307 test lines, 90%+ of additions)
- ✅ Strong design principles (SOLID score: 9.2/10)
- ✅ High code quality (maintainability: 10/10)
- ✅ Clear objective achieved (GPU compatibility for PR pr-adding-tensors #182)
- ✅ No breaking changes (backward compatible)
- ✅ Outstanding documentation (docstrings, commit messages, examples)
Risk Assessment
Risk Level: 🟢 LOW
- All automated tests passing
- Comprehensive test coverage for new functionality
- No breaking API changes
- Clear, incremental commits that can be reverted if needed
- Author marked as "ready to merge into main"
📋 RECOMMENDATIONS FOR FUTURE PRs
Minor Improvements (Not Blocking)
-
🟡 Extract vectorization pattern into a helper function to reduce duplication
- Priority: Low
- Effort: 1-2 hours
- Benefit: Improved DRY compliance, easier maintenance
-
🟡 Add inline comments for complex substitution logic
- Priority: Low
- Effort: 30 minutes
- Benefit: Improved readability for new contributors
-
🟡 More specific error messages for constraint type validation
- Priority: Low
- Effort: 30 minutes
- Benefit: Better debugging experience
-
🟡 Consider adding
@inferredtests for performance-critical functions- Priority: Low
- Effort: 1 hour
- Benefit: Ensure type stability, catch performance regressions
-
🟡 Consider renaming
subs2mtosubs_midpointor add alias- Priority: Very Low
- Effort: 15 minutes
- Benefit: Slightly clearer naming
These are minor improvements and should NOT block this PR.
📊 COMMIT HISTORY ANALYSIS
Commit Quality: ✅ Excellent
The PR has 12 well-structured commits with clear progression:
- Initial fixes: Bug fixes and refactoring
- Test additions: Comprehensive test coverage
- Test corrections: Fixing test expectations for new behavior
- Integration: Merging and final adjustments
Commit Message Quality: Outstanding
- Clear, descriptive headlines
- Detailed explanations in commit bodies
- References to related issues/PRs
- Co-authored commits properly attributed
Example of Excellent Commit Message:
Add tests for :other constraint type and fix superscript exponents
- Add comprehensive tests for :other constraint type errors in test_onepass_fun_bis.jl and test_onepass_exa_bis.jl
* Unit tests for p_constraint_fun!/p_constraint_exa! with :other type
* Integration tests using @def and @def_exa macros for invalid constraints
* Tests cover mixed state/control at different times (e.g., x(0)*u(t)+u(t)^2 <= 1)
- Fix incorrect superscript exponents in test_onepass_fun.jl
* Replace r²(__s) with (r^2)(__s) in lines 760 and 854
* Ensures proper aliasing for exponentiated variables
- Update as_range tests in test_onepass_exa_bis.jl
* Reflect new implementation returning :(($x):($x)) instead of [x]
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
📋 SUMMARY FOR AUTHOR
Excellent work, @jbcaillau! 🎉
This PR demonstrates:
- ✅ Careful bug analysis and fixing
- ✅ Comprehensive test coverage
- ✅ Clean refactoring and code improvement
- ✅ Strong adherence to software engineering best practices
- ✅ Clear documentation and commit messages
- ✅ Effective collaboration (co-authored commits)
Key Achievements
- Critical Bug Fixes: Fixed GPU-blocking issues in ExaModels backend
- Test Coverage: Added 1,307 lines of comprehensive tests
- Code Quality: Improved maintainability with better utilities and refactoring
- Documentation: Excellent docstrings with examples
- Preparation: Laid groundwork for tensor implementation (PR pr-adding-tensors #182)
Impact
This PR successfully achieves its objective of fixing GPU compatibility issues and preparing the codebase for the tensor implementation work in PR #182. The code is production-ready, well-tested, and maintainable.
Next Steps
- ✅ Merge this PR into main
- Continue with tensor implementation in PR pr-adding-tensors #182
- Consider the minor improvements listed above for future PRs
Status: ✅ APPROVED - Ready to merge into main
📚 REFERENCES
- PR Vectorisation bis #201: Vectorisation bis #201
- Issue [Dev] Adding tensors #181: [Dev] Adding tensors #181
- PR pr-adding-tensors #182: pr-adding-tensors #182
- Design Principles Reference:
.codeium/windsurf/global_workflows/design-principles-reference.md - Julia Commands Reference:
.codeium/windsurf/global_workflows/julia-commands-reference.md - Git/GitHub Common:
.codeium/windsurf/global_workflows/git-github-common.md
📎 APPENDIX A: Understanding @inferred Tests for Type Stability
What is @inferred?
@inferred is a Julia macro (from the Test package) that verifies type stability of a function. It ensures that the Julia compiler can statically infer the return type of a function without ambiguity.
Why Type Stability Matters
In Julia, type stability is crucial for performance. The compiler can generate highly optimized machine code when it knows the types at compile time.
Example of Type Instability (BAD):
# ❌ Type unstable - compiler cannot predict return type
function bad_function(x)
if x > 0
return x # returns Int
else
return 0.0 # returns Float64 - DIFFERENT TYPE!
end
end
# Compiler sees: return type = Union{Int, Float64}
# Result: Slower code with runtime type checksExample of Type Stability (GOOD):
# ✅ Type stable - compiler knows return type
function good_function(x)
if x > 0
return Float64(x) # returns Float64
else
return 0.0 # returns Float64 - SAME TYPE!
end
end
# Compiler sees: return type = Float64
# Result: Fast, optimized machine codeHow to Use @inferred
In your test suite, add type stability tests:
using Test
@testset "Type Stability Tests" begin
# Test that as_range always returns the same type
@test (@inferred as_range(5)) == :(5:5)
@test (@inferred as_range(1:10)) == 1:10
# Test that is_range always returns Bool
@test (@inferred is_range(1:10)) === true
@test (@inferred is_range(5)) === false
endRecommended Tests for PR #201
For the functions introduced in this PR, here are suggested @inferred tests:
1. Testing as_range - Critical Utility Function
@testset "as_range type stability" begin
# Test with scalar input
@test (@inferred as_range(5)) == :(5:5)
# Test with range input
@test (@inferred as_range(1:10)) == 1:10
# Test with expression input
@test (@inferred as_range(:(x:y))) == :(x:y)
end2. Testing is_range - Frequently Used Predicate
@testset "is_range type stability" begin
# Must always return Bool
@test (@inferred is_range(1:10)) === true
@test (@inferred is_range(5)) === false
@test (@inferred is_range(:(1:10))) === true
endComplete Example for test/test_utils.jl
Here's how to add these tests to your existing test file:
@testset "Type Stability" begin
@testset "is_range type inference" begin
# AbstractRange
r = 1:10
@test @inferred(is_range(r)) === true
# Scalar
@test @inferred(is_range(5)) === false
# Expression
expr = :(1:10)
@test @inferred(is_range(expr)) === true
end
@testset "as_range type inference" begin
# Scalar input
result1 = @inferred as_range(5)
@test result1 == :(5:5)
# Range input
result2 = @inferred as_range(1:10)
@test result2 == 1:10
# Expression input
result3 = @inferred as_range(:(x:y))
@test result3 == :(x:y)
end
endWhat Happens if @inferred Fails?
If the test detects type instability, you'll see an error like:
ERROR: return type Expr does not match inferred return type Union{Expr, UnitRange{Int64}}This means:
- The compiler cannot determine the return type at compile time
- Julia must perform runtime type checks
- Performance is degraded (potentially 10-100x slower)
Benefits for CTParser.jl
- Early Detection: Catches performance regressions before they reach production
- Documentation: Explicitly documents that these functions are type-stable
- Performance Guarantee: Ensures Julia compiler optimizations work correctly
- CI/CD Integration: Tests fail if someone breaks type stability
Why This is a "Minor" Recommendation
This is marked as minor because:
- Your functions (
as_range,is_range) are likely already type-stable - They are simple and the Julia compiler optimizes them well
- This is more a preventive best practice than a bug fix
- The code works correctly without these tests
However, it's an excellent practice for high-quality Julia packages, especially those focused on performance (like optimal control solvers).
Performance Impact Example
# Without type stability
function unstable_sum(n)
result = 0
for i in 1:n
result = i > n/2 ? result + i : result + Float64(i) # Type changes!
end
return result
end
# With type stability
function stable_sum(n)
result = 0.0 # Start with Float64
for i in 1:n
result = result + Float64(i) # Always Float64
end
return result
end
# Benchmark results (typical):
# unstable_sum: ~500 ns
# stable_sum: ~50 ns (10x faster!)Additional Resources
- Julia Performance Tips: https://docs.julialang.org/en/v1/manual/performance-tips/#Write-%22type-stable%22-functions
- @code_warntype: Use this macro to check type stability manually
- Test.@inferred: https://docs.julialang.org/en/v1/stdlib/Test/#Test.@inferred
Recommended Next Steps
- Add
@inferredtests foras_rangeandis_range - Run
@code_warntypeon performance-critical functions - Consider adding these tests to CI/CD pipeline
- Document type stability requirements in function docstrings
📎 APPENDIX B: Extract Vectorization Pattern into Helper Function
The Problem: Code Duplication
In the current implementation, the vectorization pattern is repeated in multiple places throughout src/onepass.jl:
Repeated Pattern (appears 4+ times):
k = __symgen(:k)
e2 = subs2(e2, xt, p.x, j)
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
e2 = subs2(e2, ut, p.u, j)
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))Locations where this pattern appears:
p_constraint_exa!- Line ~803 (constraints)p_dynamics_coord_exa!- Lines ~905, ~911, ~917 (dynamics with different schemes)p_lagrange_exa!- Lines ~982, ~988 (Lagrange cost)p_mayer_exa!- Lines ~1048, ~1051 (Mayer cost)
Why This Violates DRY
Don't Repeat Yourself (DRY) principle states:
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."
Repeating this pattern:
- ❌ Makes maintenance harder (need to update in multiple places)
- ❌ Increases risk of inconsistencies
- ❌ Makes the code harder to understand
- ❌ Increases the chance of bugs
Proposed Solution: Helper Function
Create a dedicated helper function in src/utils.jl:
"""
$(TYPEDSIGNATURES)
Vectorize state and control expressions for ExaModels backend.
This function performs a two-step substitution:
1. Scalar indexing: `x[i]` → `x[i, j]` and `u[i]` → `u[i, j]`
2. Bare symbols: `x` → `[x[k, j] for k ∈ 1:dim_x]` and `u` → `[u[k, j] for k ∈ 1:dim_u]`
This is necessary for ExaModels to properly handle vector-valued states and controls
in GPU-compatible code generation.
# Arguments
- `e::Expr`: Expression to vectorize
- `xt::Symbol`: Temporary symbol for state (e.g., `:xt`)
- `ut::Symbol`: Temporary symbol for control (e.g., `:ut`)
- `p::ParsingInfo`: Parsing context containing state/control info
- `j::Union{Symbol,Expr,Int}`: Time index (e.g., `:j1`, `0`, `:grid_size`)
# Returns
- `Expr`: Vectorized expression
# Example
```julia
e = :(xt[1] + ut[2])
p = ParsingInfo(x = :x, u = :u, dim_x = 2, dim_u = 2)
result = vectorize_state_control(e, :xt, :ut, p, :j)
# Returns: :([x[k, j] for k ∈ 1:2][1] + [u[k, j] for k ∈ 1:2][2])See also: subs2, subs
"""
function vectorize_state_control(e, xt, ut, p, j)
k = __symgen(:k)
# Step 1: Substitute scalar indices for state
e = subs2(e, xt, p.x, j)
# Step 2: Handle bare state symbols (vectorization)
e = subs(e, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
# Step 3: Substitute scalar indices for control
e = subs2(e, ut, p.u, j)
# Step 4: Handle bare control symbols (vectorization)
e = subs(e, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))
return e
end
### Usage in `src/onepass.jl`
**Before** (in `p_constraint_exa!`):
```julia
:state_fun || :control_fun || :mixed => begin
code = :(length($e1) == length($e3) == 1 || throw("this constraint must be scalar"))
xt = __symgen(:xt)
ut = __symgen(:ut)
e2 = replace_call(e2, [p.x, p.u], p.t, [xt, ut])
j = __symgen(:j)
k = __symgen(:k)
e2 = subs2(e2, xt, p.x, j)
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
e2 = subs2(e2, ut, p.u, j)
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))
e2 = subs(e2, p.t, :($(p.t0) + $j * $(p.dt)))
concat(
code,
:($pref.constraint(
$p_ocp, $e2 for $j in 0:grid_size; lcon=($e1[1]), ucon=($e3[1])
)),
)
end
After (cleaner, DRY-compliant):
:state_fun || :control_fun || :mixed => begin
code = :(length($e1) == length($e3) == 1 || throw("this constraint must be scalar"))
xt = __symgen(:xt)
ut = __symgen(:ut)
e2 = replace_call(e2, [p.x, p.u], p.t, [xt, ut])
j = __symgen(:j)
# Use helper function for vectorization
e2 = vectorize_state_control(e2, xt, ut, p, j)
e2 = subs(e2, p.t, :($(p.t0) + $j * $(p.dt)))
concat(
code,
:($pref.constraint(
$p_ocp, $e2 for $j in 0:grid_size; lcon=($e1[1]), ucon=($e3[1])
)),
)
endBenefits
- ✅ DRY Compliance: Single source of truth for vectorization logic
- ✅ Maintainability: Update once, affects all usages
- ✅ Readability: Intent is clear from function name
- ✅ Testability: Can unit test the vectorization logic independently
- ✅ Documentation: Centralized explanation of the vectorization process
- ✅ Consistency: Ensures all vectorizations follow the same pattern
Testing the Helper Function
Add tests in test/test_utils.jl:
@testset "vectorize_state_control" begin
# Setup parsing info
p = ParsingInfo()
p.x = :x
p.u = :u
p.dim_x = 2
p.dim_u = 2
# Test basic vectorization
e = :(xt[1] + ut[2])
xt = :xt
ut = :ut
j = :j
result = vectorize_state_control(e, xt, ut, p, j)
# Verify the structure (exact match may vary due to gensym)
@test result isa Expr
@test occursin("x", string(result))
@test occursin("u", string(result))
endImplementation Steps
- Add
vectorize_state_controlfunction tosrc/utils.jl - Update
p_constraint_exa!to use the helper - Update
p_dynamics_coord_exa!to use the helper - Update
p_lagrange_exa!to use the helper - Update
p_mayer_exa!to use the helper - Add unit tests for the helper function
- Run full test suite to ensure no regressions
Estimated Effort
- Time: 1-2 hours
- Risk: Low (well-isolated change)
- Impact: High (improves maintainability significantly)
📎 APPENDIX C: Add Inline Comments for Complex Substitution Logic
The Problem: Complex Logic Without Explanation
The substitution logic in ExaModels backend functions is complex and not immediately obvious to readers unfamiliar with the codebase.
Current Code (in p_constraint_exa!):
e2 = subs2(e2, xt, p.x, j)
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
e2 = subs2(e2, ut, p.u, j)
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))Questions a reader might have:
- Why are there two substitutions for
xtandut? - What's the difference between
subs2andsubs? - Why do we need the array comprehension
[$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]? - What is the purpose of this transformation?
Proposed Solution: Strategic Inline Comments
Add clear, concise comments explaining the why and what of each step:
# Vectorization for ExaModels backend:
# Step 1: Transform scalar-indexed expressions (e.g., x[i] → x[i, j])
e2 = subs2(e2, xt, p.x, j)
# Step 2: Transform bare symbols to vector comprehensions (e.g., x → [x[k, j] for k ∈ 1:n])
# This is necessary for ExaModels to handle vector-valued states in GPU code
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))
# Step 3: Same transformation for control variables
e2 = subs2(e2, ut, p.u, j)
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)]))Guidelines for Effective Comments
✅ Good Comments (Explain WHY and WHAT)
# Convert time-dependent expressions to grid-indexed form for discretization
e2 = subs(e2, p.t, :($(p.t0) + $j * $(p.dt)))
# Ensure scalar bounds for path constraints (ExaModels requirement)
lcon=($e1[1]), ucon=($e3[1])
# Generate unique symbol to avoid variable name collisions
k = __symgen(:k)❌ Bad Comments (State the obvious)
# Substitute e2
e2 = subs2(e2, xt, p.x, j)
# Set k to symgen
k = __symgen(:k)
# Call subs
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)]))Specific Locations to Add Comments
1. In p_constraint_exa! (Line ~796)
:state_fun || :control_fun || :mixed => begin
code = :(length($e1) == length($e3) == 1 || throw("this constraint must be scalar"))
xt = __symgen(:xt)
ut = __symgen(:ut)
e2 = replace_call(e2, [p.x, p.u], p.t, [xt, ut])
j = __symgen(:j)
k = __symgen(:k)
# Vectorization for ExaModels: Transform expressions to grid-indexed form
# This enables GPU-compatible code generation for vector-valued states/controls
e2 = subs2(e2, xt, p.x, j) # x[i] → x[i, j]
e2 = subs(e2, xt, :([$(p.x)[$k, $j] for $k ∈ 1:$(p.dim_x)])) # x → [x[k,j] for k ∈ 1:n]
e2 = subs2(e2, ut, p.u, j) # u[i] → u[i, j]
e2 = subs(e2, ut, :([$(p.u)[$k, $j] for $k ∈ 1:$(p.dim_u)])) # u → [u[k,j] for k ∈ 1:m]
# Convert continuous time to discrete grid points
e2 = subs(e2, p.t, :($(p.t0) + $j * $(p.dt)))
concat(
code,
:($pref.constraint(
$p_ocp, $e2 for $j in 0:grid_size; lcon=($e1[1]), ucon=($e3[1])
)),
)
end2. In p_dynamics_coord_exa! (Line ~902)
# Discretization schemes for dynamics
# Each scheme requires different time point evaluations
j1 = __symgen(:j)
j2 = :($j1 + 1) # Next grid point
j12 = :($j1 + 0.5) # Midpoint (for midpoint/trapeze schemes)
k = __symgen(:k)
# Evaluate dynamics at j1 (current grid point)
ej1 = subs2(e, xt, p.x, j1)
ej1 = subs(ej1, xt, :([$(p.x)[$k, $j1] for $k ∈ 1:$(p.dim_x)]))
ej1 = subs2(ej1, ut, p.u, j1)
ej1 = subs(ej1, ut, :([$(p.u)[$k, $j1] for $k ∈ 1:$(p.dim_u)]))
ej1 = subs(ej1, p.t, :($(p.t0) + $j1 * $(p.dt)))
# Evaluate dynamics at j2 (next grid point) for implicit schemes
ej2 = subs2(e, xt, p.x, j2)
ej2 = subs(ej2, xt, :([$(p.x)[$k, $j2] for $k ∈ 1:$(p.dim_x)]))
ej2 = subs2(ej2, ut, p.u, j2)
ej2 = subs(ej2, ut, :([$(p.u)[$k, $j2] for $k ∈ 1:$(p.dim_u)]))
ej2 = subs(ej2, p.t, :($(p.t0) + $j2 * $(p.dt)))
# Evaluate dynamics at midpoint (average of j1 and j2) for midpoint/trapeze schemes
ej12 = subs2m(e, xt, p.x, j1) # subs2m handles midpoint averaging
ej12 = subs(ej12, xt, :([(($(p.x)[$k, $j1] + $(p.x)[$k, $j1 + 1]) / 2) for $k ∈ 1:$(p.dim_x)]))
ej12 = subs2(ej12, ut, p.u, j1)
ej12 = subs(ej12, ut, :([$(p.u)[$k, $j1] for $k ∈ 1:$(p.dim_u)]))
ej12 = subs(ej12, p.t, :($(p.t0) + $j12 * $(p.dt)))3. In subs2 function documentation (src/utils.jl)
The docstring is already good, but could be enhanced:
"""
$(TYPEDSIGNATURES)
Substitute occurrences of symbol `x` in expression `e` with indexed access to `y` at time index `j`.
# Transformation Rules
Handles two patterns:
- **Scalar indexing**: `x[i]` → `y[i, j]`
- **Range indexing**: `x[1:3]` → `[y[k, j] for k ∈ 1:3]`
- **Bare symbols**: NOT substituted (use `subs` for this)
# Why Two Substitutions?
This function handles indexed access (`x[i]`), while `subs` handles bare symbols (`x`).
This separation allows fine-grained control over expression transformation, which is
necessary for ExaModels backend code generation.
# Arguments
- `e::Expr`: Expression to transform
- `x::Symbol`: Symbol to find and replace
- `y::Symbol`: Replacement symbol (typically a variable name)
- `j::Union{Symbol,Expr,Int}`: Time/grid index to add
- `k::Symbol`: (keyword) Loop variable for range comprehensions
# Examples
```@example
julia> # Scalar indexing
julia> e = :(x0[1] * 2xf[3] - cos(xf[2]) * 2x0[2])
julia> subs2(subs2(e, :x0, :x, 0), :xf, :x, :N)
:(x[1, 0] * (2 * x[3, N]) - cos(x[2, N]) * (2 * x[2, 0]))
julia> # Range indexing
julia> e = :(x0[1:3])
julia> subs2(e, :x0, :x, 0; k = :k)
:([x[k, 0] for k ∈ 1:3])
julia> # Bare symbols are NOT substituted
julia> e = :(x0 * 2xf[3])
julia> subs2(subs2(e, :x0, :x, 0), :xf, :x, :N)
:(x0 * (2 * x[3, N])) # x0 remains unchangedSee also: subs2m, subs, subs3
"""
### Benefits of Good Comments
1. ✅ **Onboarding**: New contributors understand the code faster
2. ✅ **Maintenance**: Future you remembers why the code is written this way
3. ✅ **Debugging**: Easier to spot when code doesn't match intent
4. ✅ **Code Review**: Reviewers can verify logic matches comments
5. ✅ **Documentation**: Comments complement docstrings
### When NOT to Comment
- ❌ Don't comment obvious code: `x = 5 # Set x to 5`
- ❌ Don't comment bad code: Fix the code instead
- ❌ Don't comment outdated info: Update or remove stale comments
- ❌ Don't comment what: Comment why and how
### Estimated Effort
- **Time**: 30-60 minutes
- **Risk**: None (comments don't affect functionality)
- **Impact**: Medium (improves code understanding)
---
## 📎 APPENDIX D: More Specific Error Messages
### The Problem: Generic Error Messages
Current error messages are too generic and don't help users understand what went wrong or how to fix it.
**Current Code** (in `p_constraint_exa!`):
```julia
_ => return __throw("bad constraint declaration", p.lnum, p.line)
Problems with this message:
- ❌ Doesn't say what constraint type was attempted
- ❌ Doesn't list valid constraint types
- ❌ Doesn't explain why it's invalid
- ❌ Doesn't suggest how to fix it
Proposed Solution: Descriptive Error Messages
1. In p_constraint_exa! - Invalid Constraint Type
Before:
_ => return __throw("bad constraint declaration", p.lnum, p.line)After:
_ => return __throw(
"unsupported constraint type: $c_type\n" *
"Expected one of: :boundary, :variable_fun, :initial, :final, " *
":variable_range, :state_range, :control_range, :state_fun, :control_fun, :mixed\n" *
"Check that your constraint expression is properly formed.",
p.lnum,
p.line
)Example Error Output:
ParsingError:
Line 15: x(0)*u(t) + u(t)^2 ≤ 1
unsupported constraint type: :other
Expected one of: :boundary, :variable_fun, :initial, :final,
:variable_range, :state_range, :control_range, :state_fun, :control_fun, :mixed
Check that your constraint expression is properly formed.
2. In p_constraint_fun! - Invalid Constraint Type
Before:
_ => return __throw("bad constraint declaration", p.lnum, p.line)After:
_ => return __throw(
"unsupported constraint type: $c_type\n" *
"This constraint mixes state and control at different time points, which is not supported.\n" *
"Supported constraint types:\n" *
" - :boundary: constraints on initial/final state and variables\n" *
" - :state_range, :control_range, :variable_range: box constraints\n" *
" - :state_fun, :control_fun, :mixed: path constraints (state/control at same time)\n" *
"Example of invalid constraint: x(0)*u(t) ≤ 1 (x at t=0, u at t=t)\n" *
"Example of valid constraint: x(t)*u(t) ≤ 1 (both at t=t)",
p.lnum,
p.line
)3. Dimension Mismatch Errors
Before:
code = :(length($e1) == length($e3) == length($rg) || throw("wrong bound dimension"))After:
code = :(
if length($e1) != length($rg)
throw("Lower bound dimension mismatch: expected $(length($rg)) elements, got $(length($e1))\n" *
"Range: $rg\n" *
"Lower bound: $e1")
elseif length($e3) != length($rg)
throw("Upper bound dimension mismatch: expected $(length($rg)) elements, got $(length($e3))\n" *
"Range: $rg\n" *
"Upper bound: $e3")
end
)Example Error Output:
ERROR: Lower bound dimension mismatch: expected 3 elements, got 2
Range: 1:3
Lower bound: [0, 1]
4. In constraint_type - Unknown Pattern
Add more context when constraint type cannot be determined:
function constraint_type(e, t, t0, tf, x, u, v)
# ... existing logic ...
# If we reach here, constraint type couldn't be determined
error(
"Unable to determine constraint type for expression: $e\n" *
"This may indicate:\n" *
" - Mixed state/control at different times (not supported)\n" *
" - Invalid use of time variable $t\n" *
" - Undefined state ($x), control ($u), or variable ($v)\n" *
"Please check your constraint formulation."
)
end5. In p_time! - Invalid Time Declaration
Before:
_ => return __throw("bad time declaration: $t0, $tf", p.lnum, p.line)After:
_ => return __throw(
"invalid time declaration: t ∈ [$t0, $tf]\n" *
"When using variable-dependent time bounds, both t0 and tf must reference the same variable.\n" *
"Valid examples:\n" *
" - t ∈ [v[1], v[2]], time (both from variable v)\n" *
" - t ∈ [0, v[1]], time (t0 constant, tf from variable)\n" *
" - t ∈ [v[1], 10], time (t0 from variable, tf constant)\n" *
"Invalid example:\n" *
" - t ∈ [v[1], w[1]], time (different variables v and w)",
p.lnum,
p.line
)Error Message Best Practices
✅ Good Error Messages
- State what went wrong: "unsupported constraint type: :other"
- Explain why it's wrong: "This constraint mixes state and control at different times"
- List valid options: "Expected one of: :boundary, :initial, :final, ..."
- Provide examples: "Example of valid constraint: x(t)*u(t) ≤ 1"
- Suggest a fix: "Check that your constraint expression is properly formed"
❌ Bad Error Messages
- ❌ "Error" (no information)
- ❌ "bad input" (too vague)
- ❌ "constraint error" (what kind?)
- ❌ "invalid" (invalid how?)
Implementation Template
function better_error_message(context, details, suggestions)
"""
$context
Details:
$details
Suggestions:
$suggestions
"""
end
# Usage
__throw(
better_error_message(
"Unsupported constraint type: $c_type",
"This constraint mixes state and control at different times, which is not supported.",
"Use constraints where state and control appear at the same time point, e.g., x(t)*u(t) ≤ 1"
),
p.lnum,
p.line
)Testing Error Messages
Add tests to verify error messages are helpful:
@testset "Error Messages" begin
@testset "Constraint type error message" begin
err = try
@def begin
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
x(0)*u(t) ≤ 1 # Invalid: mixed times
∂(x)(t) == [u(t), 0]
end
catch e
e
end
@test err isa ParsingError
@test occursin("unsupported constraint type", err.msg)
@test occursin(":other", err.msg)
@test occursin("Expected one of", err.msg)
end
@testset "Dimension mismatch error message" begin
err = try
@def_exa begin
t ∈ [0, 1], time
x ∈ R³, state
-1 ≤ x[1:3] ≤ [1, 2] # Invalid: 3 elements vs 2 bounds
∂(x)(t) == [0, 0, 0]
end init = (nothing, zeros(3, 101), nothing) grid_size = 100
catch e
e
end
@test occursin("dimension mismatch", lowercase(string(err)))
@test occursin("expected 3", lowercase(string(err)))
@test occursin("got 2", lowercase(string(err)))
end
endBenefits
- ✅ Better User Experience: Users understand errors immediately
- ✅ Faster Debugging: Less time spent figuring out what went wrong
- ✅ Self-Documenting: Error messages serve as inline documentation
- ✅ Reduced Support: Fewer questions about error meanings
- ✅ Learning Tool: Users learn correct syntax from error messages
Estimated Effort
- Time: 1-2 hours
- Risk: None (only changes error messages)
- Impact: High (significantly improves user experience)
Priority Locations
- High Priority:
p_constraint_exa!andp_constraint_fun!(most common errors) - Medium Priority:
p_time!, dimension checks - Low Priority: Other validation errors
Review Completed: 2025-12-31 23:10 UTC+01:00
Reviewer: Cascade AI Code Review
Workflow: /pr-code-review
Review Duration: ~10 minutes
Lines Analyzed: 1,273 net changes across 12 files
Uh oh!
There was an error while loading. Please reload this page.