13
$\begingroup$

There are so many false calculations out there that working with Quantity und units—at least initially—should be mandatory in many applications.

Alas, implementation in the Wolfram Language sometimes seems to be in the way. I will give a simple calculation of compound interest as example:

r =  Quantity[ 5, "Percent"/"Years" ]; (* a continuously compounding rate *)

t = Quantity[ 10, "Years" ]; (* time for interest to accrue *)

initialCapital = Quantity[ 1000, "USDollars" ]; 

initialCapital Exp[ r t ]

Result

While in this special case using Normal and N come to the rescue, it seems that the treatment of units that are compatible to "DimensionlessUnit" like Percent is not consistent accross WL functions.

Note the following:

Log[ 1. + 5 Quantity["Percent"] ]
(* 0.0487902 *)

Log[ 1. + Quantity[ 5, "Percent"] ]
(* 0.0487902 *)

Exp[ 5. Quantity["Percent"] ]
(* E^Quantity[5., "Percent"] *)

Is there some principle here? How to best work with units in these cases?


A support case with the identification [CASE:4935014] was created. Please include this ID in any future correspondence with Wolfram Technical Support regarding this issue.

$\endgroup$
10
  • 3
    $\begingroup$ Log and Exp do seem to have different opinions on whether "Percent" quantities should go through Normal or not. Until Exp figures it out you might be better off always normalizing it yourself: r = Normal[Quantity[5, "Percent"]]/Quantity["Years"] $\endgroup$
    – Jason B.
    Apr 26, 2022 at 11:32
  • $\begingroup$ 1. + 5 Quantity["Percent"] evaluates to a pure number, 1.05. So Exp[1. + 5 Quantity["Percent"]] evaluates to a pure number. Log and Exp aren't really involved with that example, since the Quantity[] goes away. For the last example, Log[Quantity[5, "Percent"]] also does not evaluate. I thought Exp and Log took dimensionless arguments, although percent could be argued to be dimensionless. Perhaps Quantity[] objects are treated as having dimensions, but 0. + Quantity[5, "Percent"] suggests special cases exist. $\endgroup$
    – Michael E2
    Apr 26, 2022 at 13:20
  • 2
    $\begingroup$ Interestingly, most (transcendental) functions do not treat "Percent" as a pure number, including Power. Some exceptions are the trig. functions except Sec and InverseCDF[NormalDistribution[], r t]. There may be others but I stopped looking. It's odd that five of the six trig. funs. handle "Percent". -- As for aiding humanity: (1b) Strip "Percent" from input and internally avoid percent; put it back in output when appropriate...(5) Report to WRI and wait for them to fix it (probably the slowest workaround, but also the best solution assuming no other). $\endgroup$
    – Michael E2
    Apr 26, 2022 at 14:04
  • 1
    $\begingroup$ I think it does NOT make sense to Exp or Log a quantity with units. There may be an equivalence between a percentage and a pure number, but the moment you state Quantity[5,"Percent"] you are stating a unit not a number. I don't think it's reasonable to expect Mathematica to deal with the concept behind the units, or attempts to enforce normal use or physics. I think that what the OP is asking is not something that would be desirable for Mathematica to provide. $\endgroup$
    – rhermans
    Apr 26, 2022 at 16:20
  • 1
    $\begingroup$ @rhermans I beg to differ: The unit associated with Quantity[ 5, "Percent" ] is "DimensionlessUnit" and there is a reason that it can also be represented by 1. All we should ask the WL to do is to consistently treat such quantities. Your appraoch is fair, but it is not the way WL treats all of these cases, which is clearly confusing. $\endgroup$
    – gwr
    Apr 26, 2022 at 16:24

3 Answers 3

8
$\begingroup$

While the approaches in the comments (thanks a lot!) solve the issue, the question triggered me to try to "have my cake and eat it as well", i.e., enter quantities but decide globally whether they are used and what will happen if I just want magnitudes—after all using Quantity slows down things.

A Litte Package For Conditional Quantities

The use of quantities with appropriate units should be mandatory for any mathematical model of reality, so that we may have a chance to validate equations.

While using Quantity is the way to go, there is a price to pay with regard to performance—likely a reason that using Quantity may be avoided and mere comments are used for units, which have the downside, that we can't let Mathematica help us.

So a nice thing to have may actually be to allow the verbose use of Quantity so that users can enter 10 Quantity["Percent"] instead of 0.1 (don't laugh, it happens). But at the same time, we may use ConditionalQuantity as a wrapper to guide conversion with regard to the use in our models.

The following package thus has two main functions:

  1. Allow to use On["Quantities"] and Off["Quantities"] to switch between UnitConvert and QuantityMagnitude for quanties as appropriate.
  2. Tell Mathematica how we would like to have our verbose units be interpreted either for UnitConvert or QuantityMagnitude.

We can achieve (2) by using:

  • ConditionalQuantity[quantity] to use QuantityMagnitude as is or to keep quantity as is, i.e., no target units are provided.
  • ConditionalQuantity[quantity, "Canonical" ] to try to go for the canonical unit, if it exists.
  • ConditionalQuantity[quantity, "Normal" ] to get rid of all units that are compatible to "DimensionlessUnit" (i.e., replace those units by 1), but leave the other units unchanged.
  • ConditionalQuantity[quantity, unit] to tell QuantityMagnitude and UnitConvert that unit is the target unit. (Instead of unit we also provide a reference Quantity.)

Code

BeginPackage[ "ConditionalQuantities`" ]

Quantity::inuse = "Quantities in use."

QuantitiesOffQ::usage = "\
QuantitiesOffQ[] returns True, if messages related to Quantities have been switched off using Off[\"Quantities\"].\
The function will by default be used by ConditionalQuantity to decide whether a magnitude or a quantity is to be returned."

$numericalUnitRules = "\
$numericalUnitRules is a list of replacement rules for numerical units like Percent, BasisPoints, Thousand etc."

ConditionalQuantity::usage = "\
ConditionalQuantity[quantity] will return QuantityMagnitude[quantity] if QuantitiesOffQ[] is True and quantity, if this is not the case.\n\
ConditionalQuantity[quantity, \"Canonical\" ] will call ConditionalQuantity[quantity, unit], where unit is the canonical unit.\n\
ConditionalQuantity[quantity, \"Normal\" ] will call ConditionalQuantity[quantity, unit], where unit is QuantityUnit[quantity] where all parts\
 compatible to \"DimensionlessUnit\" have been replaced by 1.\n\
ConditionalQuantity[quantity, refQuantity] will call ConditionalQuantity[quantity, QuantityUnit[refQuantity]].\n\
ConditionalQuantity[quantity, unit] will return QuantityMagnitude[quantity,unit] or UnitConversion[quantity, unit] depending on QuantitiesOffQ[].\
 If units is are not compatible, unit input is disregarded and ConditionalQuantity[quantity] is called."

Begin["`Private`"]

$numericalUnitRules = Map[
    Rule[ #, 1 ]&,
        {
            "Percent",
            "BasisPoints",
            "Dozen",
            "Hundred",
            "Thousand",
            "HundredThousand",
            "Million",
            "Billion",
            "Trillion"
        }
]

$MessageGroups = Join[ $MessageGroups, { "Quantities" :> { Quantity::inuse } } ]

QuantitiesOffQ[] := HoldPattern[ Quantity::inuse ] /. Messages[ Quantity ] // Not @* FreeQ[ $Off ]

ConditionalQuantity[ q_Quantity ] := If[ QuantitiesOffQ[], QuantityMagnitude @ q, q ]

ConditionalQuantity[ q_Quantity, refq_Quantity ] := ConditionalQuantity[ q, QuantityUnit @ refq ]

ConditionalQuantity[ q_Quantity, "Canonical" ] := Enclose[
    With[
        {
            canonicalUnit = ConfirmQuiet[
                QuantityVariableCanonicalUnit @ QuantityVariable @ UnitDimensions @ q
            ]
        }
        ,
        ConditionalQuantity[ q, canonicalUnit ]
    ]
]

ConditionalQuantity[ q_Quantity, "Normal" ] := With[
    {
        normalUnit = QuantityUnit[ q ] /. $numericalUnitRules
    }
    , 
    If[ normalUnit === 1, 
        (* then *) ConditionalQuantity[ q, "DimensionlessUnit" ],
        (* else *) ConditionalQuantity[ q, normalUnit ] 
    ]
]

ConditionalQuantity[ q_Quantity, unit_ ] /; CompatibleUnitQ[ q, unit ] unit := If[ QuantitiesOffQ[],
    (* then *)
    QuantityMagnitude[ q, unit ],
    (* else *)
    UnitConvert[ q, unit ]
]

ConditionalQuantity[ q_Quantity, unit_ ] /; Not @ CompatibleUnitQ[ q, unit ] := ConditionalQuantity[q]

End[]

EndPackage[]

Examples

Assuming that we loaded the package with <<ConditionalQuantities` or Needs, we can now do the following:

On["Quantities"] (* Not really necessary since it is default *)

r := ConditionalQuantity[ Quantity[5., "Percent"/"Years"], "Normal" ];
t := ConditionalQuantity[ Quantity[10, "Years"] ]; 
initialCapital := ConditionalQuantity[ Quantity[1000, "USDollars"] ];

initialCapital Exp[r t]
(* $1648.72 *)

r
(* 0.05 per year *)

Do[ initialCapital Exp[r t], 1000] // RepeatedTiming
{0.957131, Null}

Off["Quantities"]

initialCapital Exp[ r t ]
(* 1648.72 *)

r
(* 0.05 *)

Do[ initialCapital Exp[r t], 1000] // RepeatedTiming
{0.413421, Null}
$\endgroup$
2
  • $\begingroup$ This solution is probably overdone for simple use cases, but it may help to propagate the (important) use of Quantity to verify calculations and avoid errors in magnitude. Using QuantityMagnitude unfortunately is risky with Percent and the like. I'll make this a community answer in the hope that it'll be useful and improved upon. $\endgroup$
    – gwr
    Apr 26, 2022 at 15:28
  • $\begingroup$ Just updated the code as it is not KnownUnitQ that will matter, but rather CompatibleUnitQ. Also using Enclose to make sure there is no Failure[...] regarding the search for a canonical unit. $\endgroup$
    – gwr
    Apr 27, 2022 at 10:56
6
$\begingroup$

You can use UnitDimensions and UnitConvert to unwrap dimensionless quantities.

With r, t, and initialCapital in OP and

dimensionlessQuantityQ[q_Quantity] := UnitDimensions[q] === {}

then

N[initialCapital Exp[r t]] /. q_Quantity?dimensionlessQuantityQ :> UnitConvert[q]
Quantity[1648.7212707001281`, "USDollars"]

Hope this helps.

$\endgroup$
5
  • $\begingroup$ Thanks. This works for expressions with pure dimensionless units, but it will not get rid of embedded dimensionless units: N[Quantity[5., "Percent"/ "Year" ] ] /. q_Quantity?dimensionlessQuantityQ :> UnitConvert[q] will give 5.% per year. I should have made clearer, that I would like to have more control about magnitudes. $\endgroup$
    – gwr
    Apr 26, 2022 at 17:52
  • $\begingroup$ @gwr Yes. that is a bit of a predicament because "Percent" is implicitly carrying a divide by 100 operation with it and it makes me wonder if any other dimensionless units are doing something similar. And a quick check with dimensionless unit "Million" says they are. $\endgroup$
    – Edmund
    Apr 26, 2022 at 18:08
  • 1
    $\begingroup$ One might want to consider the difference between UnitDimensions[q] == {} and UnitDimensions[q] === {} in bad input like dimensionlessQuantityQ[Quantity[1, "foo"]]. It makes no substantial difference in the example, but it does in situations in which a non True/False result has an effect, as in If[dimensionlessQuantityQ[...],...]. $\endgroup$
    – Michael E2
    Apr 27, 2022 at 13:49
  • $\begingroup$ @MichaelE2 @Edmund Why not use CompatibleUnitQ[ q, 1 ]? $\endgroup$
    – gwr
    Apr 27, 2022 at 15:46
  • 1
    $\begingroup$ @gwr Because I know little about Quantity stuff. When you raised the issue of the rest of humanity, my first thought was why in the world would the rest of humanity ever want to deal with Quantity. :) But I work in a dimensionless field, and my view is probably from a parochial bias. BTW, CompatibleUnitQ seems to compare UnitDimensions[q] === UnitDimensions[Quantity[1, "PureUnities"]]. Didn't know "PureUnities" before. Since Quantity[1, "PureUnities"] converts to 1, which we started with, there seems to be some wasted CPU cycles in there somewhere. $\endgroup$
    – Michael E2
    Apr 27, 2022 at 16:27
0
$\begingroup$

Personally, I think it's better to be explicit in telling Mathematica when you want percents to be treated as units or as numbers. In the original post, the problem was setup with percent as a unit. At some point, the desire was to convert to a representation where the percent was a number. Rather than expect Mathematica to correctly infer when such conversions are desired, why not just be explicit? Any of the following, or any similar "forcing" to a new representation, seem appropriate to me:

initialCapital Exp[r t] /. q_Quantity :> Normal[q]
initialCapital Exp[r t] /. q : Quantity[_, "Percent"] :> Normal[q]
initialCapital Exp[r t] /. q_Quantity :> UnitConvert[q]
initialCapital Exp[r t] /. q : Quantity[_, "Percent"] :> UnitConvert[q]
$\endgroup$
2
  • 1
    $\begingroup$ You can apply Normal or UnitConvert directly, so replacement is not needed. Also note that there are "BasisPoint", "Dozen", "Thousand" etc. as well. When the result is not a numerical unit like "Percent"/ "Year" Normal will not help and UnitConvert will go for a canonical unit which converts years to seconds. Compare to the solution I posted, which does not have these downsides. $\endgroup$
    – gwr
    Apr 26, 2022 at 16:04
  • $\begingroup$ The point is to be explicit. Your assertions are not 100% correct based on my experiments, but I won't argue the details. $\endgroup$
    – lericr
    Apr 26, 2022 at 16:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.