-
Notifications
You must be signed in to change notification settings - Fork 1
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
DynamicQuantities.jl #18
Comments
I just had a look at it, but having just dimensions and not units, you seem to loose a bit of flexibility with regard to scaling and choice of units. As far as I understood it you have to have your quantities in the base SI units. You would need to involve Unitful in some manner anyway to get the parsing/conversion between different units. |
Yeah you'd need a normalization pass that converted everything just before adding to the JuMP model. So I guess something at runtime instead of what Unitful does at compile time. |
I did a quick test of using DynamicalQuantities as an extension of JuMP. The code can be found here: https://github.com/trulsf/JuMP.jl/tree/tf/dyn_quant Currently, only rudimentary support for LP/MIP, but it seems to run ok and the code is much simpler (mainly due to the fact that we do not need a wrapper for variables and constraints, but can include them directly in a The following example show how it can be used: using Unitful
using DynamicQuantities
using JuMP
using HiGHS
const DQ = DynamicQuantities
m = Model()
@variable(m, x >= 0, Dimensions(length = 1, time = -1))
@variable(m, y >= 0, convert(Dimensions, Unitful.dimension(u"m/s")))
@variable(m, z, Bin, Dimensions())
max_speed = DQ.Quantity(4, length = 1, time = -1)
@constraint(m, x + y <= max_speed * z)
obj = @objective(m, Max, x + 3y)
set_optimizer(m, HiGHS.Optimizer)
optimize!(m)
println("obj = $(value(obj))")
println("x = $(value(x))")
println("y = $(value(y))")
println("z = $(value(z))") If you think this is something to explore further, I can open a PR on JuMP for discussion or I can move it to a separate package if that is more suitable. |
The lack of scaling seems an issue, because that's the main source of bugs for JuMP users. And we don't want people using the base units if it's going to cause numerical issues for the solver. Perhaps it just needs a scale dimension? for I don't know if we need to add to JuMP immediately. I think it's probably better if we wait a few months for other packages to adopt (or not) it first to see how things pan out. |
I did another test with the recent version of DynamicQuantities (v0.4) and used a The problem currently is that values for the variables are returned in the base units, regardless of the scale used for them in the model. I guess this will require a version with a scaled quantity (as you have been discussing). The following example shows the results of running the latest version julia> using DynamicQuantities, JuMP, HiGHS
julia> m = Model(HiGHS.Optimizer);
julia> set_silent(m)
julia> @variable(m, x >= 0, u"m/s")
x [1.0 m s⁻¹]
julia> @variable(m, y >= 0, u"km/h")
y [0.2777777777777778 m s⁻¹]
julia> @variable(m, z, Bin, Dimensions())
z [1.0 ]
julia> max_speed = 4u"km/s"
4000.0 m s⁻¹
julia> @constraint(m, x + y <= max_speed * z)
x + 0.2777777777777778 y - 4000 z <= 0 [m s⁻¹]
julia> obj = @objective(m, Max, x + 3y)
0.8333333333333334 y + x [m s⁻¹]
julia> optimize!(m)
julia> println("obj = $(value(obj))")
obj = 12000.0 m s⁻¹
julia> println("x = $(value(x))")
x = 0.0 m s⁻¹
julia> println("y = $(value(y))")
y = 4000.0 m s⁻¹
julia> println("z = $(value(z))")
z = 1.0 |
Two comments:
f(v, units::Quantity) = (v .* ustrip(units), dimension(units)) Which gives you the following: julia> f(ones(3), u"km/s")
([1000.0, 1000.0, 1000.0], m s⁻¹) In other words, rather than storing the scale in the units, you just multiply any scaling outside of the SI base units into the expression. So in e.g., julia> @variable(m, y >= 0, u"km/h") it could be automatically converted into julia> @variable(m, (y/0.28) >= 0, u"m/s") I'm still not sure I fully understand the desired implementation, but perhaps with the new struct ScaledQuantity{T,D<:AbstractDimensions,S} <: AbstractQuantity{T,D}
value::T
dimensions::D
end
ScaledQuantity(v::T, d::D, ::Val{S}=Val(1.0)) where {T,D<:AbstractDimensions,S} = ScaledQuantity{T,D,S}(v, d)
ScaledQuantity(q::Quantity{T}) where {T} = ScaledQuantity(one(T), dimension(q), Val(ustrip(q)))
Base.show(io::IO, q::ScaledQuantity{T,D,S}) where {T,D,S} = print(io, ustrip(q), " [", S, " ", dimension(q), "]") where the function DynamicQuantities.constructor_of(::Type{SQ}) where {T,D,S,SQ<:ScaledQuantity{T,D,S}}
return (args...) -> ScaledQuantity(args..., Val(S))
end Then you could create quantities with, e.g., julia> ScaledQuantity(u"km/h")
1.0 [0.2777777777777778 m s⁻¹]
julia> typeof(ScaledQuantity(u"km/h"))
ScaledQuantity{Float64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}, 0.2777777777777778}
julia> ScaledQuantity(u"km/h") * 10.3
10.3 [0.2777777777777778 m s⁻¹] Though you would need to overload some functions to throw an error if |
Thanks for the feedback and the tips - as well as the great work with the DynamicQuantities package. The current implementation is based on an approach similar to your first pattern, i.e. we use a JuMP-variable as the value of a quantity by stripping off units and adding the dimension function JuMP.add_variable(
model::JuMP.Model,
x::_DQVariable,
name::String,
)
variable = JuMP.add_variable(model, x.variable, name)
return DQ.Quantity(DQ.ustrip(x.unit) * variable, DQ.dimension(x.unit))
end As stated above this works quite smoothly, but what I would like is to have the solution value in the same units as specified for the variable. If the variable is given as @variable(m, x, u"kW*h") I want the solution value to be given with the same units - not as some value times |
This is indeed tricky, as DynamicQuantities.jl converts everything into the same base units (defined by the particular I see a few options for solving this:
e.g., @variable(m, x, "kW*h") would turn into raw_string = "kW*h"
units = uparse(raw_string)
value = units * x
...
solution = ...
println(solution / units, raw_string)
|
I have done some more testing and experimenting based upon your ideas and possible options. Since I want to allow the user to combine variables with different scale of the same dimension, I have not included the scale as part of the dimension, but introduced it as a separate field of an adjusted quantity struct QuantityScale{T,D} <: DQ.AbstractQuantity{T,D}
value::T
dimensions::D
scale::Float64
end
QuantityScale(value, dim) = QuantityScale(value, dim, 1.0)
function Base.show(io::IO, qex::QuantityScale)
print(io, "$(DQ.ustrip(qex) / qex.scale) [$(qex.scale) $(DQ.dimension(qex))]")
end When creating new optimization variable, we scale the variable and keep the original scaling function JuMP.add_variable(
model::JuMP.Model,
x::_ScaledVariable,
name::String,
)
variable = JuMP.add_variable(model, x.variable, name)
scale = DQ.ustrip(x.unit)
return QuantityScale(scale * variable, DQ.dimension(x.unit), scale)
end When variables are combined into expressions and constraints, the scaling will always be kept at 1.0. julia> @variable(m, x >= 0, u"m/s")
x [m s⁻¹]
julia> x.scale
1.0
julia> @variable(m, y >= 0, u"km/h")
y [0.2777777777777778 m s⁻¹]
julia> y.scale
0.2777777777777778
julia> z = x + y
x + 0.2777777777777778 y [m s⁻¹]
julia> z.scale
1.0 After optimization the optimal value is returned as a julia> println("y = $(value(y))")
y = 14400.0 [0.2777777777777778 m s⁻¹] It is difficult to store the preferred unit (e.g. kW*h), as we only get the If you want to have a look at the full implementation, it is available here https://github.com/trulsf/JuMP.jl/blob/tf/dq_scaled/ext/units_new.jl |
I drafted some new functionality that I think will be particularly useful for your desired feature in the With the new For example: julia> using DynamicQuantities
julia> uparse_symb = DynamicQuantities.Units.UnitSymbols.uparse
uparse (generic function with 1 method)
julia> q = uparse_symb("nm * kW * h")
1.0 nm kW h
julia> q^2
1.0 nm² kW² h² Then we can convert to SI at the end: julia> convert(Quantity{Float64,Dimensions}, q^2)
1.296e-5 m⁶ kg² s⁻⁴ The downside is that when you are performing computation with this dimension space, it has to do 10x more computations, as there are 10x more fields to propagate around. But, it is good if you want to display the user's entered units which seems like what you are looking for. Thoughts? |
Thanks for following up on this! As you say, my primary use for this is to display variable values in the same units as declared by the user. With these additions I can store the original unit and use it for display purposes struct ScaledQuantity{T,D} <: DQ.AbstractQuantity{T,D}
value::T
dimensions::D
scale::Float64
unit::Union{Nothing, DQ.Quantity}
end
ScaledQuantity(value, dim) = ScaledQuantity(value, dim, 1.0, nothing)
function Base.show(io::IO, q::ScaledQuantity)
expr = DQ.ustrip(q)
if isnothing(q.unit)
print(io, "$(expr / q.scale) [$(DQ.dimension(q))]")
return
end
print(io, "$(expr / q.scale) [$(DQ.dimension(q.unit))]")
end And then convert to base SI units when calculating scale for the variable function JuMP.add_variable(
model::JuMP.Model,
x::_ScaledVariable,
name::String,
)
variable = JuMP.add_variable(model, x.variable, name)
q = convert(DQ.Quantity{Float64,DQ.Dimensions}, x.unit)
scale = DQ.ustrip(q)
return ScaledQuantity(scale * variable, DQ.dimension(q), scale, x.unit)
end Running this produces the desired output julia> usym = DynamicQuantities.Units.UnitSymbols.uparse
uparse (generic function with 1 method)
julia> m = Model(HiGHS.Optimizer);
julia> @variable(m, x >= 0, usym("m/s"))
x [m s⁻¹]
julia> @variable(m, y >= 0, usym("km/h"))
y [km h⁻¹]
julia> @variable(m, z, Bin, Dimensions())
z []
julia> max_speed = 4u"km/s"
4000.0 m s⁻¹
julia> @constraint(m, x + y <= max_speed * z)
x + 0.2777777777777778 y - 4000 z <= 0 [1.0 m s⁻¹]
julia> obj = @objective(m, Max, x + 3y)
0.8333333333333334 y + x [m s⁻¹]
julia> optimize!(m)
julia> println("obj = $(value(obj))")
obj = 12000.0 [m s⁻¹]
julia> println("x = $(value(x))")
x = 0.0 [m s⁻¹]
julia> println("y = $(value(y))")
y = 14400.0 [km h⁻¹] I think it is not ideal for the user that they need to differentiate between I am off for vacation in a few days, but I will follow up on this. I'll join the JuMPDev in Boston and will try to bring it up as a discussion topic. |
Awesome, I am glad it works! To give you another update, I just refactored julia> q = sym_uparse("5.2*nm * kW * h")
5.2 nm kW h
julia> @btime q^2
85.065 ns (5 allocations: 240 bytes)
27.040000000000003 nm² kW² h² So it is actually feasible to perform calculations directly with Regarding
If I were a user, I think I wouldn't see this choice as much of an issue. One automatically converts to SI and the other leaves it, it seems pretty reasonable to have both options. For consistency, I just exported a julia> using DynamicQuantities
julia> q = 125us"kW*h/day"
125.0 kW h day⁻¹
julia> q2 = q^2 / 5.2
3004.8076923076924 kW² h² day⁻²
julia> expand_units(q2)
5.216680021367522e6 m⁴ kg² s⁻⁶ |
Unitful has a lot of quirks.
This new package seems a lot simpler: https://discourse.julialang.org/t/ann-dynamicquantities-jl-type-stable-physical-quantities/99963
The author's other work is very solid, so it's a new package, but worth exploring.
The text was updated successfully, but these errors were encountered: