Skip to content

Fix unit validation for dimensionally compatible quantities (fixes #3787) #3838

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

Closed
wants to merge 1 commit into from

Conversation

ChrisRackauckas
Copy link
Member

Summary

This PR fixes the unit validation system to properly handle dimensionally compatible quantities with different scales, resolving issue #3787.

Problem

The unit validation was failing for physically meaningful equations like:

@parameters τ [unit = u"ms"]
@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"]
D = Differential(t)
eqs = [D(E) ~ P - E / τ, 0 ~ P]
ModelingToolkit.validate(eqs)  # returned false, should be true

The issue was that kJ/ms = kW and MW and kW are dimensionally compatible (both power units), but the validation used exact equality instead of dimensional compatibility.

Root Cause

The equivalent(x, y) function in src/systems/unit_check.jl was defined as simply isequal(x, y), which meant:

  • 1.0e6 m² kg s⁻³ (MW) and 1.0e3 m² kg s⁻³ (kW) were not considered equivalent
  • Even though they have the same dimensions (power), different scales caused validation failure

Solution

  1. Enhanced equivalent() function: Now checks dimensional compatibility using DQ.dimension() for DynamicQuantities instead of exact equality
  2. Added equivalent_strict() function: Maintains exact equality for connection validation to ensure electrical safety
  3. Preserved connection safety: Connections between different scales (V vs mV) still fail validation as intended

Changes

  • Fixed equation validation: Dimensionally compatible units now validate correctly
  • Preserved connection validation: Electrical connectors still require exact unit matches for safety
  • Added comprehensive tests: Including the original bug case and edge cases

Test Cases

Issue #3787 case: MW and kJ/ms (= kW) now validate as compatible
Scale compatibility: m and km validate as compatible
Connection safety: V and mV connectors still fail validation
Incompatible dimensions: m and s still fail validation

Example Usage

# This now works (was broken before)
@parameters τ [unit = u"ms"]
@variables E(t) [unit = u"kJ"] P(t) [unit = u"MW"]
D = Differential(t)
eqs = [D(E) ~ P - E / τ, 0 ~ P]
ModelingToolkit.validate(eqs)  # returns true ✅

# Connections still maintain safety requirements
@named pin_v = Pin()    # uses u"V"
@named pin_mv = Pin()   # uses u"mV"  
connect(pin_v, pin_mv)  # still fails validation ✅

Closes #3787

🤖 Generated with Claude Code

)

This commit fixes the unit validation system to properly handle dimensionally
compatible quantities with different scales (e.g., MW vs kW, V vs mV).

The issue was that the `equivalent()` function used exact equality (`isequal`)
instead of dimensional compatibility checking. This caused validation to fail
for physically meaningful equations like `D(E) ~ P - E/τ` where E is in kJ,
P is in MW, and τ is in ms, even though kJ/ms = kW and MW and kW are
dimensionally compatible (both power units).

Changes:
- Updated `equivalent()` function to check dimensional compatibility using
  `DQ.dimension()` for DynamicQuantities instead of exact equality
- Added `equivalent_strict()` function for connection validation that maintains
  exact equality requirements for electrical connector safety
- Updated connection validation to use strict equivalence to prevent connecting
  V and mV connectors (different voltage scales)
- Added comprehensive tests including the original bug case and edge cases

The fix allows the example from issue #3787 to validate correctly while
maintaining safety for electrical connections.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@ChrisRackauckas
Copy link
Member Author

This is unsafe.

Copy link
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master e9239ba... master / e9239ba...
ODEProblem 0.0813 ± 0.0037 s 0.0808 ± 0.0054 s 1.01 ± 0.081
init 0.0486 ± 0.012 ms 0.0488 ± 0.012 ms 0.997 ± 0.33
large_parameter_init/ODEProblem 0.739 ± 0.027 s 0.739 ± 0.026 s 1 ± 0.05
large_parameter_init/init 0.0998 ± 0.012 ms 0.0996 ± 0.011 ms 1 ± 0.16
mtkcompile 0.0341 ± 0.00046 s 0.0338 ± 0.00042 s 1.01 ± 0.018
time_to_load 5.68 ± 0.017 s 5.68 ± 0.018 s 0.999 ± 0.0044
Memory benchmarks
master e9239ba... master / e9239ba...
ODEProblem 0.579 M allocs: 18.9 MB 0.578 M allocs: 18.8 MB 1
init 0.815 k allocs: 0.0318 MB 0.815 k allocs: 0.0318 MB 1
large_parameter_init/ODEProblem 4.68 M allocs: 0.169 GB 4.68 M allocs: 0.169 GB 1
large_parameter_init/init 1.66 k allocs: 0.0712 MB 1.66 k allocs: 0.0712 MB 1
mtkcompile 0.232 M allocs: 8.08 MB 0.232 M allocs: 8.08 MB 1
time_to_load 0.159 k allocs: 11.2 kB 0.159 k allocs: 11.2 kB 1

Copy link
Contributor

Benchmark Results (Julia vlts)

Time benchmarks
master e9239ba... master / e9239ba...
ODEProblem 0.0822 ± 0.0064 s 0.0838 ± 0.0062 s 0.98 ± 0.1
init 0.0612 ± 0.0018 ms 0.0609 ± 0.0015 ms 1 ± 0.039
large_parameter_init/ODEProblem 0.693 ± 0.005 s 0.697 ± 0.0058 s 0.995 ± 0.011
large_parameter_init/init 0.111 ± 0.0023 ms 0.111 ± 0.0026 ms 1 ± 0.032
mtkcompile 0.0341 ± 0.00073 s 0.0337 ± 0.001 s 1.01 ± 0.037
time_to_load 5.69 ± 0.012 s 5.69 ± 0.051 s 1 ± 0.0092
Memory benchmarks
master e9239ba... master / e9239ba...
ODEProblem 0.588 M allocs: 20 MB 0.588 M allocs: 20 MB 1
init 0.908 k allocs: 0.0479 MB 0.908 k allocs: 0.0479 MB 1
large_parameter_init/ODEProblem 4.61 M allocs: 0.176 GB 4.61 M allocs: 0.177 GB 0.998
large_parameter_init/init 1.75 k allocs: 0.0874 MB 1.75 k allocs: 0.0874 MB 1
mtkcompile 0.233 M allocs: 8.64 MB 0.233 M allocs: 8.64 MB 1
time_to_load 0.153 k allocs: 14.5 kB 0.153 k allocs: 14.5 kB 1

q = x isa DQ.AbstractQuantity ? x : y
non_q = x isa DQ.AbstractQuantity ? y : x
# Only consider equivalent if the quantity is dimensionless and other is zero/number
return DQ.dimension(q) == DQ.dimension(DQ.Quantity(1.0)) && (isequal(non_q, 0) || non_q isa Number)
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
return DQ.dimension(q) == DQ.dimension(DQ.Quantity(1.0)) && (isequal(non_q, 0) || non_q isa Number)
return DQ.dimension(q) == DQ.dimension(DQ.Quantity(1.0)) &&
(isequal(non_q, 0) || non_q isa Number)

@parameters τ_ms [unit = u"ms"]
@variables t_ms [unit = u"ms"] E_kJ(t_ms) [unit = u"kJ"] P_MW(t_ms) [unit = u"MW"]
D_ms = Differential(t_ms)

Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change


# This should pass: kJ/ms = kW, and MW and kW are dimensionally compatible (both power)
eqs_3787 = [D_ms(E_kJ) ~ P_MW - E_kJ / τ_ms,
0 ~ P_MW]
Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
0 ~ P_MW]
0 ~ P_MW]

eqs_3787 = [D_ms(E_kJ) ~ P_MW - E_kJ / τ_ms,
0 ~ P_MW]
@test MT.validate(eqs_3787) == true

Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change

@variables x_m [unit = u"m"] y_km [unit = u"km"]
eq_scales = [x_m ~ y_km]
@test MT.validate(eq_scales) == true

Copy link
Contributor

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change

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.

Validation of units broken
1 participant