uexpr - A Tcl package to evaluate and format expressions that may include values with units
Package uexpr provides commands to evaluate and format expressions that may include values with units. Its intended to be used as part of a program to document engineering, physical and mathematical calculations, so some things (like how negation "-" and exponents, ** or ^, interact) are different from many programming languages, including expr in TCL. It includes a fairly complete set of unit definitions (and some constants) built around a set of basis units (lbm, ft, sec, Coul, degR, deg, lbmole, or a metric equivalent kg, m, sec, Coul, degK, deg, kgmole) and commands to add more unit definitions.
Package uexpr= loads the uexpr package and aliases = to uexpr. For example:
uexpr 4*ft+5*in=m
can be written:
= 4*ft+5*in=m
Expressions consist of operands and operators sometimes separated by spaces.
Operands can be numbers or numbers with units, sometimes saved in variables or a units dictionary. uexpr does not operate on strings. Comparisons in uexpr expressions return a numeric 1.0 for true, or 0.0 for false.
Numbers start with a decimal point or digit, and may include only one decimal point.
1 1.23 .123
They may include a 10's exponent after an e or E. The exponent may be signed.
1e-3 == 0.001
Numbers can start after most operators or a space. uexpr treats all numbers as double precision reals. When a string can be interpreted as a number it will be.
3e+5e == 300000*e
The first "e" is part of a number. The second e is interpreted as a variable named e. A number followed by a variable with no operator in between implies a multiplication.
3e +5e == 3*e+5*e
3E+ 5e == 3*E+5*e
Here the space breaks up the scientific number definition, so both "e" and "E" are interpreted as variables or units. Keep this in mind if scientific notation is used in an expression and units or variables named "e or "E" exist.
A unit specifier is a simple expression containing only multiply (including implied multiply) and divide operators, numbers, unit names and variable names.
ft/sec
defines a unit of speed.
BTU/hr ft^2
defines a heat flux (energy per unit area and time). Note the space between units in the denominator is an implied multiply, which has higher precedence than the divide. See implied multiply in Operators. Unit specifiers are used in operands, and to specify the units of the results from uexpr.
Values with units are simple expressions containing a pure number followed by (or multiplied by) a unit specifier. They are returned from uexpr, and can be saved in variables referenced by other uexpr expressions.
100*BTU/hr ft^2
defines a heat flux.
Internally the value with units is a list containing a magnitude and exponents on the basis units. For example using US Customary basis units: lbm, ft, sec, ...
3ft/sec
gives a list starting with 3 0 1 -1 ...
3m/sec
gives a list starting with 9.8425... 0 1 -1 ... Note both values are speeds, and both have the same exponents on their basis units. Compatible units have identical exponents on the basis units, and can be added, subtracted and compared.
Named values can be variables from the context of the call to uexpr, or units or constants from the units dictionary. Names must start with a "_" or alphabetic character, and can contain "_" "." and alphanumeric characters. If the variable is processed by uexpr, it will be treated as a subexpression, and its result inserted into the expression being evaluated. This allows variables to hold values with units, which are returned from uexpr as simple expressions, not pure numbers.
If a name follows a "$" it is a reference to a variable in the context of the uexpr call or a local variable in a function definition. If a name does not follow a "$" it may still be a variable (see Name Resolution). If a variable name follows "$" the whole expression should be braced {}, to prevent Tcl from bypassing the variable handling by uexpr.
Unlike the rest of Tcl, variable names referenced using "$" can also include "_" and "." characters. See Name Resolution.
Tcl array members (such as a(b3) can only be read using "$". A reference without the "$" prefix will be interpreted as a function call, or an implied multiply of a variable by a subexpression.
Units and constants are defined in a units dictionary, common to all uexpr calls in an interpreter. If a name follows a single ":" it will only be looked up in the units dictionary. If a unit or constant name does not follow ":", it may be interpreted as a reference to a variable, see Name Resolution below.
:ft
look up the unit named ft in the units dictionary
If a name does not follow a "$" or ":" then the first value available from the following list is used:
If the name is in an expression defining a function (see uexpr::func) it is looked up in the function's parameter list
The name is looked up as a variable in the context of the uexpr call.
The name is looked up in the units dictionary.
An error occurs if no value was found.
If the name follows a "$" it is not looked up in the units dictionary. A name following a "$" can be a Tcl array member reference.
uexpr {3*$a(b)+1}
will multiply the value of the Tcl array "a" member "b" by 3 then add 1. Very similar to what Tcl expr would do. Without the "$" uexpr will interpret (b) as a subexpression or possibly part of a function call.
uexpr {3*a(b)+1}
where a is a variable (and not a function name) multiplies 3, a and b, then adds 1
A number immediately followed by a named value will be interpreted as if there was an implied multiply between them.
3ft+1inch
will be interpreted as 3*ft+1*inch. Note implied multiply has a higher precedence than divide so:
diam/3ft
or
diam/3 ft
will be interpreted as diam/(3*ft).
uexpr does not deal with integers and bitwise operators (& | ~ << >>), or boolean operators (! && ||) or strings (eq ne lt le ge gt), or list searches (in ni). Operators roughly in descending order of precedence:
.. index into a list or nested lists in a variable.
set v {1 2 3 4}
saves a list in the variable v
= v..2
returns the 3rd list entry (3 in this case) note the list could have been generated using the uexpr "," operator too:
set v [= 1,2,3,4]
To index into nested lists use a list of index values:
set m [= (1,2,3),(11,22,33),(111,222,333)]
saves a 3x3 nested list in m
=m..(2,0)
returns a value of 111.
This is rather fragile. If the indexes do not match the actual nested list structure, there may not be any error, and odd results will be returned.
** ^ exponentiation. The right argument (exponent) must be unitless. The left argument can have units. However if the exponent is not an integer, and the left operand has units, the result basis unit exponents may not be integers. In some cases this can cause problems. see Fractional or Non-Integer Exponents. Note exponentiations group to the right so:
2^3^2 == 2^(3^2) == 512
"implied multiply" multiplication is implied any time two values are not separated by an operator. A space between values is common, however a number followed by a named value also implies multiplication. Implied multiply has a higher precedence than multiply or divide. Useful in unit specifiers.
/ divide, has same precedence as multiply.
* multiply, has same precedence as divide.
Unary + - operators to the left of their operand have a minimum precedence between addition or subtraction and multiplication. However their effective precedence is the maximum of their precedence and the precedence of the operator to their left (if any). This means:
-2^2 == 0-2^2 == -4
This is not true for Tcl expr, however it appears to match the conventions in "Handbook of Mathematical Functions" by Abramowitz and Stegun. See the definition of the error function for an example.
+ - ++ -- addition and subtraction, left and right arguments must have compatible units. ++ and -- will also insert a line break in LaTeX formatted output.
< <= =< == != <> >= => > comparisons, return 1.0 for true, 0.0 for false. note ==, != and <> are not that useful with real numbers. Left and right arguments must have compatible units.
Comparisons can be chained. The result is true if all comparisons are true.
7<a<13
will return true (1.0) if the value of a is between 7 and 13
"," separate items in a list. Used to build lists and nested lists, such as lists of function arguments:
hypot(a,b)
Also can be used to generate lists to use with the .. operator. For example coefficients for a polynomial using the sum function:
set C [uexpr 1,2,3,4]
set x 0.1
uexpr sum(i,0,3,C..i*x^i)
will return 1.234
"=" marks a result unit specifier for an expression. For example:
uexpr 15 lbf/3 in^2=psi
would return "5.0 psi" Multiple result specifiers can be given:
uexpr::ltxExpr {15 N / 5 cm^2 = bar = Pa}
would return results in bar and pascals.
"( )" group subexpressions (as for expr).
"%" is a constant defined as 0.01 in the units dict.
45%*a = 0.45*a
Most math functions that work on real numbers in expr are available in uexpr. However they are now unit aware.
Trigonometric Functions (sin cos tan) expect a single angle argument (deg rad ...).and return a pure number.
Inverse Trigonometric Functions (asin acos atan) expect a pure number and return an angle result. atan2 expects two arguments with compatible units.
Hyperbolic functions (sinh cosh tanh) expect unitless arguments and return a unitless result.
ceil int floor round round up, toward zero, down and down to the nearest integer respectively. They accept only unitless arguments, and return an integer valued double precision real number.
hypot fmod max min accept any values as long as they have compatible units.
abs accepts one argument with any units
sqrt accepts any positive argument with any units. Returns the appropriate units. If the argument basis unit exponents are not multiples of 2, the results may be useless.
exp log log10 accept one argument with no units.
rand returns a pure number.
srand accepts any units, but only uses the argument magnitude (ignoring basis unit exponents). It returns a unitless number.
Some functions available in expr are not available in uexpr. Type conversion functions (wide entier double bool) integer functions (isqrt) number format functions (isordered issubnormal isnan is normal isinf isfinite) are not implemented.
There are additional functions not normally found in expr:
sum(var,first_index,last_index,expression) sums an expression for a range of index variable values. The first and last index values must be unitless. they will be rounded to the nearest integer. The index variable will be created if it does not exist, and will be left with its last value
sum(i,0,4,2^i)
will return 31.0
A polynomial with coefficients in c
set c [= 1,2,3,4,5]
set x 10
= sum(i,0,4,c..i*x^i)
will return 54321.0
hasUnits(expression) returns 0 if the value is a pure number.
hasUnits(3)
returns 0
units(expression) returns the basis units of the result of the expression, with a magnitude of 1. For example any value with units of a pressure (psi, bar, Pascal ...) will return the same unit expression:
units(3*psi)
returns 1*lbm / ft sec^2 for US Customary basis units.
units(expression, unit specifier) if the expression is a pure number (has no units) it is multiplied by the units specified. If the expression has units and they are compatible with the units specified, the expression result is returned unchanged. If the expression result units are not compatible with the unit specifier, an error is generated. This can be used to validate input values.
Commands supplied by this package and intended for use are:
uexpr concatenates all its arguments (as with concat), then evaluates the expression and returns one result. If the expression ends with "=" followed by a unit specifier, the result is formatted to use those units. If the units specified do not match the actual result units, they will be multiplied or divided by enough base units to make them match. If multiple result unit specifiers are given only the last is used. If no result units are specified, the resulting unit expression will contain only base units. In any case the result can be used in an expression evaluated by uexpr.
[uexpr] functions similarly to expr with several significant differences. It is not a replacement for expr. For example:
uexpr 4*in+5*cm=mm
returns something close to "151.6000...002 mm"
uexpr 15 psi = in wc = bar
returns something like "1.0342...41 bar", note only the last result unit specifier was used
uexpr 15*psi = in wc
returns something like "415.609...18 in wc". Note: "in wc" is inches water column.
ltxExpr concatenates its operands separated by a space and evaluates the expression. It returns a list of results. The list always contains the expression formatted for LaTeX, and the result as a unit expression (which can be used in an expression for uexpr or ltxExpr). In addition if the expression ends with one or more result unit specifiers, a magnitude and unit specifier (formatted for LaTeX) is returned for each result unit specifier. If any of the units specified do not match the actual result units, they will be multiplied or divided by enough base units to make them match. If no units are specified, a magnitude and unit specifier (formatted for LaTeX) containing only base units will be generated. ltxExpr is intended for use in the calc package.
uexpr::ltxExpr 3*ft=in=cm
returns "{3\cdot \mathrm{ft}} {36.0 in} 36.0 {\mathrm{in}} 91.44 {\mathrm{cm}}"
uexpr::ltxExpr formats variable names to include symbols and subscripts. The "_" prefix followed by a character will insert a LaTeX symbol, typically a Greek letter. "." indicates the following characters are part of a subscript. Multiple subscripts are allowed. For example the variable name:
_a.4
generates the following LaTeX: {\alpha _{\mathrm{4}}}
return a list of all units matching the glob pattern. Return all unit names if no pattern is given.
Returns a sorted list of all units and constants with the same dimensions (compatible units) as the result of the given expression. If no expression is given, all unitless constants are returned.
For example if no new units are defined:
uexpr::unitsLike psi
returns: atm bar Pa pascal psi Torr
uexpr::unitsLike BTU/hr
returns: hp watt
uexpr::newUnit adds another unit to the unit dictionary. The expression defines the new unit in terms of any existing base or defined unit. The unit dictionary is common to all contexts within a Tcl interpreter. These changes are global. For example a mile is defined as:
uexpr::newUnit mile 5280*ft
Note, units are very similar to variables, except: variables are looked up in the calling context of the expression evaluator. Unit definitions are global, available to any instance of uexpr in the same Tcl interpreter.
uexpr::func adds a new function, usable in any uexpr call in the same interpreter. Variables in the expression that are not in the parameter list, have the value of the variable in the calling context at the time the function is defined. Variables in the parameter list have values set at the time the function is used. If not all the parameters are supplied in the function call, the value of a variable with the same name (in the calling context) is used (it came from Roark). This allows values used by a function to be set by position (in the function call) or by name (if not supplied in the function call). For example:
set c 3 uexpr::func ss {a b} {a + b + c} set d 4 uexpr ss(d,5)
returns 12 (4+5+3)
set b 2 uexpr ss(5)
returns 10 (5+2+3), b is not given, so use the current value of that variable.
set a 1 example uexpr ss()
returns 6 (1+2+3), neither a or b is given, use the current values
set b 22 uexpr ss(4)
returns 29 (4+22+3), b is not given, so the current value is used
set c 33 uexpr ss(4,5)
returns 12 (4+5+3), the change in c is ignored as it was not a function parameter
uexpr::uset writes the result of evaluating an expression to a variable, tcl array member or into a list or nested list saved in a variable.
uexpr is not a replacement for expr.
numbers are all double precision. Mostly because many unit conversion factors are not integers.
Variable names without "$" or ":" prefixes will be looked up in the calling context or the units dictionary.
variables used in expressions can contain complete expressions, not just numbers. The variable contents will be evaluated and its value used in the expression. This will likely not work if Tcl gets to the variable first. Any expression that refers to variables with the "$" prefix should be within braces.
a variable can contain a list or nested lists of unit expressions. To get a value from a list, or nested list use the ".." operator.
There are no bit or logical operations (<< >> & | && ||).
a new operator "=" indicates a result unit specifier follows. For example to add two lengths in inches, and return the result in centimeters:
uexpr 4*in+5*in=cm
Or using ltxExpr, to get the results in cm and feet:
uexpr::ltxExpr 4*in+3*in=cm=ft
There are no type conversion functions (double entier, wide). int() exists, and returns a double with an integer value.
all functions available to uexpr and friends are in the ::uexpr::ufunc namespace.
When two values are not separated by an operator there is an implied multiply. Since variable names cannot start with a number or decimal point, "3a" will be interpreted as "3 a" or "5ft" as "5 ft". Implied multiplies have higher precedence than normal multiply (*) or divide (/).
3 ft/3 in == (3*ft)/(3*in) == 12.0
However
3*ft/3*in == 0.083333...*ft^2
Probably not what was desired.
Comparisons can be chained. 1<a<3 is true if the value in a is between 1 and 3.
Numbers can be raised to a power using "**" or "^". Currently "**" and "^" work as shown in Abramowitz and Stegun "Handbook of Mathematical Functions ..." rather than Tcl's expr.
uexpr -2^4^2
returns -65536. Where as
expr -2**4**2
returns 65536 But then both uexpr and expr return the same result for:
expr 0-2**4**2
returns -65536
In US Customary units pounds (lb) can refer to a mass or the force it exerts in a 1 g gravity field. This causes all sorts of mysterious gc factors (typically about 32.174) to appear in formulas. In the uexpr package, all US Customary mass units have an "m" suffix (lbm ozm ...) and all US Customary force units have an "f" suffix (lbf ...) the base unit name (lb, ...) is not defined on purpose, so its use will cause an undefined value error. This avoids the inevitable units mismatch which occurs further on in the calculation. oz is actually defined as a volume unit. while ozm is a mass unit.
Metric units (SI) have managed to avoid this problem (mass is Kg or gram, force is newton N), although I've started to see Kg (and sometimes Kgf) show up as a force unit in some places. It was good while it lasted.
uexpr only does scaling unit conversions automatically. In particular when degF and degC values refer to a specific temperature, uexpr can not automatically convert from one to the other. If values with degF and degC units refer to temperature changes or differences, the conversions will be correct.
uexpr can be used to convert degC to degF (and visa versa) by converting to an intermediate value in absolute temperature units (degK and degR) by adding and then subtracting the absolute temperature for the 0 temperature to the degC or degF value. For example to convert from degC to degF, first convert to degK (add zero deg C in degK) then convert to degF by subtracting zero degF in degR and asking for temperature in degF:
uexpr 20*degC+zdc-zdf=degF
Similar logic holds for gauge, absolute and differential pressures. The offset from absolute to gauge pressure is atm (one atmosphere).
An expression and its result have self consistent units if the result magnitude does not change when units are removed from all values in the expression (as would happen if evaluated with expr). For example the volume of a rectangular solid:
v=h*w*l
if h, w, l are given in inches, and the volume is given in cubic inches (in^3) the units are self consistent. Given this self consistent formula, the uexpr package can calculate the volume in any desired units as long as h, w and l are all given in length units.
Many common formulas use customary units. To use them with this package they need to be written in self consistent form. One simple way to do this is divide all variables in the expression by the units specified, then multiply the expression by the specified result units. For example a formula to calculate liquid flow through a valve for simple cases (no cavitation or flashing, low viscosity) is:
Q = Cv*sqrt(DP/Sg)
where
Q is volumetric flow rate in gpm
DP is pressure drop across the valve in psi
Sg is liquid specific gravity relative to water
Cv is the valve flow coefficient (vendor supplied number)
The self consistent version of this equation is:
Q = Cv*sqrt(DP/psi/Sg)*gpm
Using this formula with uexpr the DP and flow rate can be in any compatible units:
set DP 5bar
set Sg 1.0
Cv*sqrt(DP/psi/Sg)*gpm=ft^3/sec
will calculate the flow of water (Sg = 1.0) in cubic feet per second from the pressure drop in bar.
Many formulas are simpler or have no odd looking "unit factors" when they are derived using self consistent units. One example is flow through a venturi flow meter:
Using US Customary units: pressure drop (DP) in inches of water column at 68 degF (in*wc68 in uexpr), density (den) in lbm/ft^3, meter inlet (D) and throat (d) diameter in inches, and flow rate (w) in lbm/hr (lbm is pounds mass, as opposed to lbf pounds force in uexpr). Cd is a meter factor normally near 1.0. Y is a correction for compressibility of gases, use 1.0 for liquids or gases when the operating pressure is much higher that the pressure drop.
w = 0.09970190*C.d*Y*d^2*sqrt((DP*den)/(1-(d/D)^4))
The self consistent version of this formula is:
w = sqrt(2)*C.d*Y*pi/4*d^2*sqrt((DP*den)/(1-(d/D)^4))
the only vaguely mysterious number in the self consistent formula is sqrt(2). It will work with inputs and results in any compatible units. Meter inlet and throat size in any combination of ft, in, cm... DP could be given as psi, Pascal, bar, mm mercury ... Density (den) could be given as lbm/ft^3, gram/cm^3 ... The resulting mass flow rate can be lbm/hr gram/sec ...
Often a formula for SI units will be self consistent, mass Kg, lengths meter, time sec, force newton (N), pressure Pascal (Pa) ... But it needs to be checked.
In practical formulas, fractional exponents on values with units rarely occur. Although mathematicians and physicists keep trying.
The internal representation of a value with units is a list of double precision real numbers. The first entry is a magnitude, the others are exponents on the basis units. All the unit definitions (initially, and hopefully even after additions) have integer valued exponents. The only operations that can result in non integer exponents is exponentiation or the pow() function with a non integer exponent. Small truncation and rounding errors in the exponents can build up as calculations progress. When results are generated for specified units, any left over basis unit terms are appended to the result unit specifier. If the added term has an exponent in the range -0.01 to 0.01, it is set to zero. Since most results are for values with integer exponents, this tends to purge the system of round off errors. The truncation limit can almost certainly be reduced further, if results with basis unit exponents less than +/-0.05 prove useful. For example:
(27ft)^(1/3) == 3.0*ft^0.333333
If that result is cubed, the exponent on feet would be 0.999999, however the residual exponent after calculating the answer in feet is -0.000001, within the -0.01 to 0.01 range for it to be ignored, giving the exact result 27.0*ft.
Formulas using customary units may end up with fractional exponents. The calculation of liquid flow rate through a valve above is an example. The procedure to format the formula for self consistent units, results in all calculations being done with unitless values, so there is no problem with fractional basis unit exponents.
Some formulas in fluid mechanics and heat transfer raise unitless numbers such as Reynolds number, Prandtl number and Nusselt number to odd decimal fractional powers. However as these are unitless parameters, there is no problem with fractional exponents on basis units.
Another example is modeling a compressor as an isentropic or polytropic process, where volume ratios are raised to powers between 1.4 and 1.1 (isentropic volume exponent). Once again the volume ratio is unitless, and there are no fractional exponents on the basis units.
Calculations, uexpr
Copyright © 2021-2024 J.D Bruchie (BSD License)