-
-
Notifications
You must be signed in to change notification settings - Fork 209
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
[WIP] Optional unit rewriting #1252
Conversation
…write (although that should be avoided anyway b/c the rewrite will post the top-level unit and `get_unit` can just check for this first now). It is also helpful for debugging/comprehension to see the conversion factors with units.
…_unit` still won't validate them correctly.
…he units work out correctly on their own.
This I really want to make it work b/c it gives two capabilities:
I can work around (2) by letting One could argue that (1) isn't really a problem, but it's annoying/confusing to be required to make a parameter Edit: actually, I can't work around (2) if the rewriting is optional & is done by the user before constructing the system. Reason being, any rearranging of the equations that gets done in an outer constructor loses the units that |
@YingboMa @shashi @ChrisRackauckas Any preference about whether:
|
Well it should always be correct by default. If you check for whether the users uses units first, and then check if the user needs conversions, there should be no extra cost from what we had before and it would be correct right? |
No question there.
True. The 1st question is whether to automatically insert those conversions during checking. @isaacsas was concerned about the performance of this approach, and requested I make the insertion of conversion factors a standalone transformation that the user would need to apply to the equations before they would pass the checking. I was of the opinion that it would be more convenient if the conversion was done automatically during checking. |
Just to clarify, I was thinking this could be a system transformation. i.e. a user constructs a system and gets an inconsistent units warning, with the advice to try calling |
But that would mean a system with incorrect units would get constructed instead of automatically being fixed as @ChrisRackauckas mentioned. |
@isaacsas I see. I think that even with warnings, it would be better not to allow construction of a system that would be incorrect if simulated. What's the reason to prefer the transformation to be applied to the system instead of the set of equations? Edit: for concreteness, here's the workflow for using this PR: @variables t [unit = u"ms"] P(t) [unit = u"MW"] E(t) [unit = u"J"]
@parameters τ [unit = u"ms"] γ
D = Differential(t)
eqs = [D(E) ~ P - E/τ]
@test_throws MT.ValidationError ODESystem(eqs,name=:sys) # Existing unit check prevents this construction
neweqs = MT.rewrite_units(eqs) # Apply transformation to the equations
@named sys = ODESystem(neweqs) # Unit check passes |
Sure, that ordering is fine. I missed your earlier edit about having rewriting before system construction not working, so thought that was the workflow. I don't want to monopolize the discussion here too much. @ChrisRackauckas and @YingboMa are probably much more familiar with the full breadth of MT users and what approach makes the most sense as the default. I would defer to their preferred approach. I just wanted to make sure that the performance implications were at least being considered in the proposed workflow / design. (And maybe there aren't any relative to just checking the units, which would be great.) |
The problem is that generating the problem without adding in the conversions is just a silent wrong behavior which is really bad. That's the kind of thing that should error. But as mentioned earlier, it should only incur a cost when the conversions are actually required, so there shouldn't be anything to worry about in performance if it's only done when the user is specifically requiring this feature. But this means it's crucial to set it up to first check if units exist, and then check if units are same vs compatible, and then do conversions only when necessary. The scan to turn off this pass should be rather quick. |
Ahh, I hadn't thought of doing it that way. Maybe this is naive, but my expectation was that the two passes (checking-only vs rewriting) would be about the same speed (especially when nothing needed to be changed), so we might as well do the rewrite by default. OTOH, the checking won't allocate but the rewriting will, so I could be wrong. I should really try profiling these things so we can be quantitative about the performance. |
I have a system with 15 relatively simple equations/states (18 parameters), 5 equations of which throw unit errors when doing validation. julia> @btime MT.validate(eqs)
... #warning messages elided
912.514 μs (2658 allocations: 149.98 KiB) versus julia> @btime MT.rewrite_units(eqs)
342.558 μs (1629 allocations: 83.23 KiB) That's ... interesting. Trying a different system that already has matching units (9 equations, 24 parameters): julia> @btime MT.validate(gas_eqs)
560.897 μs (4167 allocations: 212.22 KiB) versus rewriting: julia> @btime MT.rewrite_units(gas_eqs)
633.992 μs (3741 allocations: 174.11 KiB) I guess I did a bad job implementing |
Weird, though you should make sure to interpolate your variables into @btime MT.validate($eqs) |
@anandijain are there larger SBML models that would have units we could try this out with? |
@isaacsas SciML/SBMLToolkit.jl#20 While SBML.jl does store unit information, SBMLToolkit doesn't do anything with it yet. |
I'm testing the idea of having unit conversion factors added as new 'anonymous' parameters during the rewrite inside the system construction. This allows to make the conversion factors clear, such that using ModelingToolkit, NonlinearSolve, Unitful
@unit qe "qe" ElectronCharge u"q" false
vars = @variables(begin
v, [unit = u"m/s"], # particle velocity
ρ, [unit = u"m"], # particle gyroradius
S #ratio of device radius to particle gyroradius
end)
pars = @parameters(
r = 1.0, [unit = u"m"], # device radius
E = 100, [unit = u"keV"], # particle energy
B = 1, [unit = u"T"], # magnetic field
m = 1.0, [unit = u"u"], # particle mass
qₑ = 1, [unit = qe], # particle charge
μ_0 = ustrip(Unitful.μ0), [unit = unit(Unitful.μ0)]
)
eqs = [
S ~ r/ρ,
ρ ~ m*v/(qₑ*B),
E ~ 0.5*m*v^2,];
@named ns = NonlinearSystem(eqs, vars, pars) Now: julia> MT.equations(ns)
3-element Vector{Equation}:
0 ~ r*(ρ^-1) - S
0 ~ var"1.0364269652680505e-8 qe s T u⁻¹"*m*v*(B^-1)*(qₑ^-1) - ρ
0 ~ 0.5var"1.0364269652680506e-11 s² keV m⁻² u⁻¹"*m*(v^2) - E
julia> MT.parameters(ns)
8-element Vector{Any}:
r
E
B
m
qₑ
μ_0
var"1.0364269652680505e-8 qe s T u⁻¹"
var"1.0364269652680506e-11 s² keV m⁻² u⁻¹" |
How to handle extra non-user parameters was something that I was also struggling with and thinking about in #1246 The issue is that |
@isaacsas My first attempt was to make a new subtype |
Yeah, I think the conclusion in that issue I linked was that we'll ultimately need some changes to support internal parameters in |
The closure would have to be a RuntimeGeneratedFunction. |
@test_throws MT.ValidationError ODESystem(eqs,name=:sys) # Existing unit check prevents this construction
neweqs = MT.rewrite_units(eqs) # Apply transformation to the equations
@named sys = ODESystem(neweqs) # Unit check passes this can be automated using a |
Constant(x) = Constant(x, Dict(VariableUnit => Unitful.unit(x))) | ||
Base.:*(x::Num, y::Unitful.Quantity) = value(x) * y | ||
Base.:*(x::Unitful.Quantity, y::Num) = x * value(y) | ||
Base.show(io::IO, v::Constant) = Base.show(io, v.val) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should probably show unit if it's there. Good way to know it's not just a number.
For validation I'm guess you just need a dimensionality check, that can be done faster by propagating 7 rational numbers. If you don't work with Unitful at that level, then it should be type stable. Chain([@rule ~x*~y => set_dim(~x*~y, map(+, dimension(~x), dimension(~y))
@rule ~x + ~y => throw(ValidationError()) where dimension(~x) != dimension(~y)
...]) |> Postwalk ^ this should do the validation. |
I think the fastest would be something like this: struct Dim
t::NTuple{7, Rational{Int}}
end
Dim(u::Unit) = .... # make a Dim
function dim(x)
if istree(x)
if operation(x) == (+)
ds = map(dim, arguments(x))
if !all_equal(ds)
return ValidationError("...")
end
return first(ds)
elseif operation(x) == (*)
return add(map(dim, arguments(x)))
# ... other cases
else
return Dim((0,0,0,0,0,0,0))
end
elseif x isa Sym
return Dim(ModelingToolkit.get_unit(x))
end
end |
Closing because stale. I'll try again once I get a solution for #1802. |
This is an alternative to #1250 that preserves the original unit validation & merely adds an optional rewrite tool for equations that can be used prior to constructing a system from the equations. Prompted by @isaacsas.
Pros:
Cons:
get_unit
andconstructunit
(some reuse though)get_unit
to find those.)