Closed rmsrosa closed 3 years ago
Something like that is not implemented right now, but I think it would be nice to have.
Hi, thanks for replying!
We have implemented this equivalency in UnitfulBrew.jl
, which is going to be, of course, an extension with quantities related to beer brewing.
I will be happy to make a PR with that, including the equivalence between wavenumber and frequency, which is used in astropy.unit (and because both units are already in Unitful.jl
).
UnitfulBrew.jl
is our first repo in JuliaBrewing.
I think this would be nice as a separate package, e.g. UnitfulEquivalencies
. I actually started to design such a package, maybe we can combine our ideas. I will try to upload my implementation in the next few days, hopefully we can discuss it then. Here are some ideas/comments:
@equivalency MassEnergy Unitful.Energy/Unitful.Mass = Unitful.c0^2
that would create the appropriate conversion functions. Here, MassEnergy
is the name of the equivalency.
MassEnergy
equivalency also enable conversion between, e.g., kg/s
and J/s
(mass-per-time and energy-per-time)? This might only be easily possible when the equivalency is a proportional relation.I am indeed using a global list of equivalencies, but I agree that it might lead to conflict. But it might be good to have an intermediate solution, allowing different lists. The advantage is that we don't need to keep remembering the name of the conversion function for each quantity. And depending on the field of application, it might be interesting to set a default list of equivalencies so we don't need to keep passing the list all the time.
In my case, I defined a function econvert
to handle the equivalencies, instead of adding another dispatch for uconvert
.
Currently, econvert
reads a global list of equivalencies, but we may add an argument to econvert
that could either be a given list or a given equivalence conversion function (with a default list if nothing is given, as mentioned above).
For defining the equivalency functions, I defined a macro more or less as you suggested, but using units instead of dimensions:
@equivalence °P sg plato_to_sg
@equivalence sg °P sg_to_plato
@equivalence ppm Unitful.mg/Unitful.L x::Quantity -> x.val * Unitful.mg/Unitful.L
@equivalence Unitful.mg/Unitful.L ppm x::Quantity -> x.val * ppm
It is not clear to me whether it is best to use dimensions or units. I needed to use units since both `°P` and `sg` above are non-dimensional.
- The second argument of `@equivalence` is unnecessary since it can actually be inferred from the function, but I left it like that for a start, but it makes sense to drop it and have the macro figure it out.
- I haven't thought about derived units :-|
PS: I must also admit my lack of discernment between using equivalence/equivalency. I have been going back and forth with it.
I am indeed using a global list of equivalencies, but I agree that it might lead to conflict. But it might be good to have an intermediate solution, allowing different lists. The advantage is that we don't need to keep remembering the name of the conversion function for each quantity. And depending on the field of application, it might be interesting to set a default list of equivalencies so we don't need to keep passing the list all the time.
One equivalency can include several conversion functions (like in astropy.units, where the spectral()
equivalency can convert between energy, frequency, wavelength, and wavenumber), so one doesn’t have to remember individual conversion functions (those would be hidden from the user). Of course, one could still add the possibility to have lists of multiple equivalencies and a default list.
In my case, I defined a function
econvert
to handle the equivalencies, instead of adding another dispatch foruconvert
.
I have done the opposite (defined a uconvert
method with three arguments, the third being the equivalency), but I would be fine with econvert
.
It is not clear to me whether it is best to use dimensions or units. I needed to use units since both
°P
andsg
above are non-dimensional.
I chose dimensions, since these equivalencies aren’t tied to specific units (it doesn’t matter whether the energy is given in J
or eV
, it can be converted to a wavelength in any case). Your case is arguably a bit special – units that share the same dimension (in your case dimensionless), but with non-trivial conversion between them. I would argue that sg
and °P
should have their own dimensions to avoid such errors. Otherwise, how do you keep people from using uconvert
on these units and getting a wrong result?
I haven't thought about derived units :-|
Since “derived equivalencies” are only well-defined for proportional equivalencies (I think), I would simply not include them for now.
PS: I must also admit my lack of discernment between using equivalence/equivalency. I have been going back and forth with it.
I just used equivalency because that’s what astropy.units uses, I didn’t give it much thought. Equivalence is shorter and seems like the more common word, so maybe we should use that instead.
I’ve uploaded my implementation here: https://github.com/sostock/UnitfulEquivalences.jl
Great! I am learning from it!
I see you've only implemented proportional and anti-proportional equivalences. I don't know whether you plan to implement more general equivalences. The one we need between degrees Plato and specific gravity is a quadratic relation. I guess this case can be included in line 109 of your code. Instead of throwing an error if it is not of any of the two cases, it implements a different relation as given by the expression. In this case, of course, we would need to add two equivalences, the direct y = f(x) conversion and the inverse x = f^-1(y), since the code will not be able to figure this out in general.
As for my use of units instead of dimensions, at the end it is about the same, because I actually infer the dimension from the unit and use uconvert to change to the unit used in the conversion function. So, there is no limitation from using units.
And although I agree that using dimension might be more elegant, I am still not sure it is the best solution. I still need to think what to do in our case that both Plato and specific gravity are nondimensional. I think it is artificial to add different units to them, or at least to one of them, to make it work with your implementation. Using units avoids this.
Now, since I am new to Julia, there are things in your code that I don't know how they work. In line 47, you call edconvert
for the equivalence conversion, while in line 28 you define edconvert
but the only it has it to throw an ArgumentError
. Can you explain me how it works, please?
Now, since I am new to Julia, there are things in your code that I don't know how they work. In line 47, you call edconvert for the equivalence conversion, while in line 28 you define edconvert but the only it has it to throw an ArgumentError. Can you explain me how it works, please?
Ok, I think I got it. it is just another dispatch throwing an error in case the dispatch with the proper dimensions, defined in _eqrelation
, is not found, right? Great.
Ok, I think I got it. it is just another dispatch throwing an error in case the dispatch with the proper dimensions, defined in
_eqrelation
, is not found, right? Great.
Yes, that's how it works. The macros @equivalence
and @eqrelation
both use _eqrelation
to define two edconvert
methods to convert between the equivalent quantities.
For equivalences that are not (anti-)proportional, you would not use the macros but instead just add the edconvert
methods yourself. This would be properly documented once we agree on a design, so users know how to add their own equivalences.
Maybe we can continue the discussion at sostock/UnitfulEquivalences.jl?
Yes, let us continue there. I will close the issue, then.
Hi, Developers,
I am new to Julia and I just heard about Unitful.jl.
I wonder whether Unitful.jl has methods to handle equivalencies as in astropy.units (see https://docs.astropy.org/en/stable/units/equivalencies.html).
I search the issues for "equivalent", "equivalencies", "equivalence", and quickly browsed the documentation and the code for that and didn't find anything, but I apologize if this is implemented in some way and I just missed it.
Thanks in advance!