Units: a new pack for units and quantities

Hello everybody,

I have recently got down a new interesting rabbit hole with unit aware arithmetics.
In short, doing operations with metre, inch, seconds…
After finding out the excellent mp-units c++ library, I have decided to simply copy their system built on units and quantities.
Basically, in addition to units, you can also add “quantities” informations to your operation making them more safe and instructive.
I have managed to make a pack called units which you can use very simply by wrapping any arithmetic operation with the qeval/1 wrapper:

:- use_module(library(units)).

% simple numeric operations
?- qeval(10*km =:= 2*5*km).

% conversions to common units
?- qeval(1 * h =:= 3600 * s).
?- qeval(1 * km + 1 * m =:= 1001 * m).

% derived quantities
?- qeval(1 * km / (1 * s) =:= 1000 * m / s).
?- qeval(2 * km / h * (2 * h) =:= 4 * km).
?- qeval(2 * km / (2 * km / h) =:= 1 * h).

?- qeval(2 * m * (3 * m) =:= 6 * m^2).

?- qeval(10 * km / (5 * km) =:= 2).

?- qeval(1000 / (1 * s) =:= 1 * kHz).

% assignement and comparison 
?- qeval(A is 10*m), qeval(A < 20*km).
A = 10 * kind(isq:length)[si:metre].

You can convert from one unit to another with the in/3 predicate and convert between quantities with the as/3 predicate:

?- qeval(Speed is (m/s in inch/h) as isq:speed).
Speed = 141732.28346456692 * isq:speed[international:inch/si:hour].

The README goes a bit more in depth in the different features of the library.
Unfortunately, the library needs a very new swi-prolog (>= ‘9.3.15’) because I use the new SSU syntax for DCG, but you can already try the pack by downloading it through the pack system:

?- pack_install(units).

One thing that I am quite proud of is that the library is also compatible with clpBNR !

?- qeval({A*metre == B*inch}), A = 1.
A = 1,
B = 5000r127.

Please, try it and let me know what you think.
If you have any improvements or suggestions, I am listening.
If you want to help, drop me a message here or on github.
There are still a lot of quantities and units that I haven’t added from the original mp-units library.
And of course, lot of documentation to write ^^

There are loads of implementation details I would love to discuss.

For example, tabling was a life saver in this implementation for memoizing and improving the determinism of many predicates.
Moreover, some predicates does term rewriting which can results in cycles, but all of that goes away with tabling !

I am quite proud of my neat normalization predicate which works with factors only (so no additions):

?- units:normalize(a*b*c*a*A*1/(b*f), E).
E = A*a^2*c/f.

Another predicate I am proud of is the predicate for finding a common unit for 2 different units:

?- units:common_unit(si:kilo(metre), F1, international:inch, F2, U).
F1 = 10^3,
F2 = 9144/(12*3*10000),
U = si:metre.

It uses iterative deepening for finding the shortest amount of transformation to get to a common unit.

2 Likes

Good to see a comprehensive library on this issue!

I’m not sure whether or not it is a good idea, but you can define the units as postfix operators, so you can write 10km instead of 10*km

That looks amazing! I wish I had a project I could use it in right now, but I’ll certainly install it just for those times when I need to do some unit conversions in a friendlier way than with Emacs’ calc!

Hum, I’m not sure that is a good idea.
With only just the si and internatioinal system of units, I can count 1 271 different units !
and if you want symbols like km for kilo(metre), you can double that number ^^

1 Like

I guess it would become 10 kilo metre, reading as metre(kilo(10)).

I’ll think about it.
Maybe I could make it optional and only use it if the user explicitly import those operators :thinking:

Could be very useful; a couple of general comments:

?- qeval({A*metre == B*inch}), A = 1.
A = 1,
B = 5000r127.

I like this because it can be used as a simple universal units converter, e.g.,

?- qeval({1.0*m =:= X*in}).
X = 39.37007874015748.

It would be preferable IMO if using CLP could be deferred until runtime (no special syntax), i.e., if an evaluation would generate an instantiation error, apply a constraint instead. I think a simple ground/1 would suffice as a test.

Also, there is a considerable database of information about units (kind, system, names) that might be useful. It would be nice to be able to query that independent of any evaluation. All the pieces are probably there but it wasn’t immediately obvious how to use it.

I’m not sure I correctly understand the question, but the way I implemented qeval is to defer evaluation at the very end. One can use the internal eval_/2 predicate for this:

?- units:eval_({metre == B*inch}, E),
  % this shows how I use the `q` dict internally to store the value, quantity and unit information
  E = q{v: {1==5000r127*9144/(12*3*10000)}, q: isq:length, u: si:metre},
  % evaluate the value B
  call(E.v).
B = 5000r127,
E = {1==5000r127*9144/(12*3*10000)} * isq:length[si:metre].

One annoying thing is that since clpBNR does not have an assignment operator, we can’t do something like this:

?- qeval({X == A*metre + B*inch}).
ERROR: Domain error: `q{q:1,u:1,v:_6690}' expected, found `_6720+_6732*9144/(12*3*10000) * isq:length[si:metre]'

Since it assume that X is a unitless quantitiless value.
@ridgeworks Do you have any suggestion on a nicer way to do something like this:

126 ?- units:eval_(1*metre + 1*inch, Expr),
    qeval({X*Expr.u == Expr}).
Expr = 1+9144/(12*3*10000) * isq:length[si:metre],
X = 5127r5000.

I’ll try to document everything soon. I’ll annouce when progress is done :slight_smile:

You can already do a lot with the common_quantity/3 and common_unit/5 predicates:

131 ?- units:common_unit(si:metre/si:hour, F1,
                         international:inch/si:second, F2,
                         CommonUnit).
F1 = 1/(60*60),
F2 = 9144/(12*3*10000),
CommonUnit = si:metre/si:second.

?- units:common_quantity(isq:area, isq:width^2, CommonQuantity).
CommonQuantity = isq:length^2.

So let me pose it a different way. Why shouldn’t the above example expressed as the following work:

[debug]  ?- qeval(1.0*m =:= X*in).
ERROR: Arguments are not sufficiently instantiated
ERROR: In:
ERROR:   [14] 1.0=:=_18232*9144/(... * 10000)
ERROR:   [13] '<meta-call>'(('.'(...,v,...),call(...))) <foreign>
ERROR:   [12] units:qeval(1.0*m=:=_18338*(in)) at /Users/rworkman/.local/share/swi-prolog/pack/units/prolog/units.pl:387
ERROR:   [11] toplevel_call(user:user: ...) at /Applications/SWI-Prolog9.3.19.app/Contents/Resources/swipl/boot/toplevel.pl:1319
   Exception: (14) 1.0=:=_5476*9144/(12*3*10000) ? abort
% Execution Aborted

It appears that the exception is caused by the variable in the expression 1.0=:=_5476*9144/(12*3*10000). Now if this case could be detected, e.g., using ground/1 and alternatively execute the following:

?- { 1.0=:=_5476*9144/(12*3*10000)}.
_5476 = 39.37007874015748.

that would achieve my objective, i.e., I wouldn’t need to use {..} to express constraints in qeval.

Since “assignment” doesn’t have a mathematical interpretation, I’m not sure what you mean. Indeed, Prolog doesn’t support assignment; just unification.

What clpBNR doesn’t support is non-arithmetic terms, e.g., units, in constraints; its domain is just the extended set of real numbers. I think the issue here is how to synergistically combine the two.

I have just modified my qeval/1 predicate to this:

qeval(Expr) =>
   eval_(Expr, Q),
   (  (ground(Q.v) ; subsumes_term({_}, Q.v))
   -> call(Q.v)
   ;  call({Q.v})
   ).

And now it works:

?- qeval(1.0*m =:= X*in).
X = 39.37007874015748.

Let me reformulate: altough clpBNR equality is super useful for doing unit conversion by specifying arbitrary units on both side of the equation, it is difficult to compute the result of an expression with an arbitrary combination of units.
Something like “compute the results of Xkg + Yg and express in a reasonable unit”.
The library has actually the code for doing this for the is operator:

?- qeval(X is 1.0*m + 1*in).
X = _A * isq:length[si:metre],
_A:: 1.025400000000000... . % funny, since X is not ground, it uses clpBNR for the evaluation
% wait, what ? So clpBNR does have an `is` operator ?
?- qeval({X is 1.0*m + Y*in}).
X = _A * isq:length[si:metre],
_A::real(-1.0Inf, 1.0Inf),
Y::real(-1.0Inf, 1.0Inf).

Ok, well, I have answered my own question ^^

Well is is just a synonym for == (as is =:=); they all mean mathematical equality. So I’m not sure it does what you want. As I tried to say the problem is with kg (in general, Exp * Units), which is not numeric. So you have to extract the units and incorporate conversion into the mathematical expresssion before evaluation - it seems like you’re doing that already.

So I’m not looking for a way to incorporate units into clpBNR. But if there’s a useful addition to clpBNR that makes the evaluation of unit expressions easier, I would happy to consider it.

I did not know that the operator is was a synonym for == (edit: for clpBNR).
So, in this case, everything works out really well since for units, I have to differentiate between is and =:= and nothing needs to change in clpBNR.

However, I have found a small pb with exponents.
clpBNR only works with the ** operator while I exclusively use the ^ operator.
Is there a reason why you only support ** ?

In SWI-Prolog they are synonyms. In ISO Prolog, ** is for floats and ^ is for integers (if I recall well). The SWI-Prolog implementation takes all numerical arguments and returns using the most precise numerical type, i.e., an integer if the result is integer, a rational if that can be computed exactly and a float as last resort.

It isn’t really. The right side of is is evaluated as an arithmetic expression and then unified with the left side.

Ah okay, so that is maybe why clpBNR only works with ** since it only manipulates floats ?
I suppose I’ll have to support both operators for the best user experience…

Yeah, its pretty silent, if you accidentially use (^)/2 instead of (**)/2:

?- P::real(0,1), time((global_maximum(P*(1-P^2)*(1-P^3), Q), solve(P))).
% 836 inferences, 0.000 CPU in 0.000 seconds (0% CPU, Infinite Lips)
false.

But I think this is old school errors = failure ?

1972 - Alain Colmerauer designs the logic language Prolog. His goal is to create a language with the intelligence of a two year old. He proves he has reached his goal by showing a Prolog session that says “No.” to every query.

it never gets old

I was more refering to this thread:

Errors considered harmful
https://swi-prolog.discourse.group/t/errors-considered-harmful/3574

Which probably gives an understanding why clp(BNR)
silently fails, although it could maybe also throw an error if it
sees an arithmetic expression, it cannot deal with.

But I am not 100% sure where the error is. If (^)/2 is supposed
to deal with integers, what does that mean? I find this
discrepancy of (^)/2 and (**)/2:

With (^)/2:

?- P::real(0,1), {1 < P^2}.
false.

?- P::real(0,1), {1 >= P^2}.
false.

With (**)/2:

?- P::real(0,1), {1 < P**2}.
false.

?- P::real(0,1), {1 >= P**2}.
P::real(0, 1).

And I don’t understand it.

It wasn’t in the original CLP(BNR) ca. 1990 and, until now, no one has asked for it. It’s a trivial one liner to add it as a synonym to **. I will do so in the next release and can tell you how to patch your the version in your library if you’d like it sooner.

But it is in clpBNR since CLP can deal with variables in arithmetic expressions.

Not true; integers, rationals, and floats are all subsets of the extended real domain. Bounds of intervals can be any numeric type supported by SWIP. clpBNR variables can be constrained to be integers. This is all facilitated by SWIP’s polymorphic implementation of arithmetic as @jan described.

Errors are non logical, so I confess to being old school. If you want noisy failure, use debug(clpBNR):

?- debug(clpBNR).
true.

?- P::real(0,1), time((global_maximum(P*(1-P^2)*(1-P^3), Q), solve(P))).
% Add {_8666{real(-1.0Inf,1.0Inf)}== -_8502{real(0,1)}*(1-_8502{real(0,1)}^2)*(1-_8502{real(0,1)}^3)}
% {} failure due to bad or inconsistent constraint: {_8666{real(-1.0Inf,1.0Inf)}== -_8502{real(0,1)}*(1-_8502{real(0,1)}^2)*(1-_8502{real(0,1)}^3)}
% 4,989 inferences, 0.002 CPU in 0.002 seconds (69% CPU, 3053244 Lips)
false.

?- P::real(0,1), {1 < P**2}.
% Add {1<_10752{real(0,1)}**2}
% ** fail ** 1<_11300{real(0,1)}.
false.

?- P::real(0,1), {1 >= P**2}.
% Add {1>=_12238{real(0,1)}**2}
P::real(0, 1).

Other debug aids are also available such as setting watch points on specific variables or tracing the fixed point propagation. And if you really want to get into the weeds, it’s all Prolog so standard debugging and profiling apply. So I see no justification for errors (as expressed in my old rant).