39
$\begingroup$

I've been playing with the new unit support in Mathematica 9. It seems very useful, but the syntax is very verbose. Instead of typing:

UnitConvert[Quantity[1, "Meters"/"Seconds"^2]*Quantity[1, "Minutes"]^2, "Kilometers"]

I would much rather type and read something like:

UnitConvert[1 m/s^2*(1 min)^2, km]

My first idea was to simply define variables for the units I'm going to use:

m = Quantity["Meters"];
km = Quantity["Kilometers"];
s = Quantity["Seconds"];
min = Quantity["Minutes"];

but unfortunately, this doesn't really work: The term 0 [any unit] is always simplified to 0, and following computations won't work because the units don't match. So for example UnitConvert[1 m/s^2*(1 min)^2, km] works fine, but UnitConvert[1 m/s^2*(0 min)^2, km] doesn't work, because the first argument to UnitConvert evaluates to 0

Are there other ways to achieve this? For example,

  • is it possible to prevent the simplification 0 * 1 Meters -> 0
  • is it possible to adjust generalized input so that entering "5 s" would evaluate to Quantity[5, "Seconds"], (like entering $d_x y$ evaluates to Dt[y,x] or n! evaluates to Factorial[n])

Of course, I've tried the Ctrl= input form first. It's a great way to learn new syntax by example, but I don't think it's practical for day-to-day use, for a number of reasons:

  • I can't use notebook expressions in the freeform input. For example:

    enter image description here

  • I can't use 2D input, so I can't even type e.g. $\partial _t$ or $\int _a^b$ within a freeform expression

  • Which means that for a longer expression I might have to enter several freeform-input in a single line, which doesn't make it more readable.
  • If I do use 2D input like $\int _a^b$, I can't enter freeform-input for a and b (EDIT: Turns out I can. I just have to enter a space before Ctrl=. Thanks @Itai Seggev)
  • I've been playing with it for an hour. It hung several times and crashed once (Not reproducible) and I had to restart it once.
  • This may be a bit philosophical: I'm using a programming language because I want to express an idea unambiguously. I don't want it to guess whether the symbol t means a variable for time or metric tons.
  • The freeform-boxes look weird in a presentation or publication. Of course, I can convert them to input or display form easily, but (in the right context) an expression like 1920*1080 Bytes*24/s might mean something to the reader, but 2.0739999999999998*^6B*(24/1s) doesn't, even if it's the same value.

UPDATE: Based on @Leonid's code, this is the best solution I've come up with so far:

ClearAll[withUnits];
SetAttributes[withUnits, HoldAll];
withUnits[code_] := ReleaseHold[(Hold[code] /.
      {
       m -> Quantity["Meters"],
       s -> Quantity["Seconds"],
       km -> Quantity["Kilometers"]
       })
    //.
    {
     Power[Quantity[m_, u_]^i_, j_] :> Quantity[m^(i*j), u^(i*j)],
     Times[x_, Quantity[m_, u_]^(i_: 1)] :> Quantity[x*m^i, u^i]
     }];

It works for the (few) examples I've tried, like

withUnits[a m/s^2 * (3s)^2] /. a -> {0, 1, 2}

but I'm not sure if the Power/Times replacement really covers all cases. Maybe someone can find counterexamples or improve it.


Using @Leonid's answer and this answer by rm -rf, I started a package MyUnits that looks like this:

BeginPackage["MyUnits`"]

Unprotect/@{Quantity,Times};
Quantity/:(0|0.) Quantity[_,unit_]:=Quantity[0,unit]
Protect/@{Quantity,Times};

meter=Quantity["Meters"];
second=Quantity["Seconds"];
hertz=Quantity["Hertz"];
minute=Quantity["Minutes"];
hour=Quantity["Hours"];
byte=Quantity["Bytes"];
kilobyte=Quantity["Kilobytes"];
megabyte=Quantity["Megabytes"];

EndPackage[ ]

Using that, I get the simple input I had with the old Units package (including command completion) and things like 0 second + 1 hour still work.

$\endgroup$
12
  • 1
    $\begingroup$ I wouldn't like to turn so many symbols into operators. But if you can live with a couple of Esc, it is definately simple to add an input alias for a nice small box that displays as you like and parses as Quantity $\endgroup$
    – Rojo
    Commented Nov 28, 2012 at 20:15
  • $\begingroup$ @Rojo: I have no idea how to do that. Could you add your suggestion as an answer? $\endgroup$ Commented Nov 28, 2012 at 20:25
  • $\begingroup$ Definately but I won't have either time or a PC for another 2 hours. If that happens and noone has posted it I will $\endgroup$
    – Rojo
    Commented Nov 28, 2012 at 20:43
  • 1
    $\begingroup$ @nikie have you tried Jon McLoone's units package? You can probably find it in the Wolfram Library. $\endgroup$ Commented Nov 29, 2012 at 11:29
  • 1
    $\begingroup$ While you can't use 2D input inside Ctrl-=, you can use Ctrl-= anywhere in 2D input. There is a bug in how Ctrl-= interacts with [PlaceHolder], but if you type space first and then Ctrl-=, you use it integrals, partial derivatives, and pretty much anywhere. $\endgroup$ Commented Dec 4, 2012 at 5:20

8 Answers 8

23
$\begingroup$

Here is a cheap way which does not involve WA, but will only be as good as you make it to be (so that you'd have to customize it yourself): create a dynamic environment:

ClearAll[withUnits];
SetAttributes[withUnits, HoldAll];
withUnits[code_] :=
  Function[Null,
     Block[{Quantity},
       SetAttributes[Quantity, HoldRest];
       Quantity /: UnitConvert[arg_, Quantity[_, unit_]] :=
          UnitConvert[arg, unit];
       Quantity /: Times[0, Quantity[_, unit_]] :=
          Quantity[0, unit];
       With[{
          m = Quantity[1, "Meters"], 
          s = Quantity[1, "Seconds"],       
          min =  Quantity[1, "Minutes"],
          km = Quantity[1, "Kilometers"]
        },
       #]],
    HoldAll][code];

So that

withUnits[UnitConvert[1 m/s^2*(1 min)^2,km]]

(*  18/5km  *)

You can set $Pre = withUnits, if you want to save some typing. The above function is a hack, of course, but it does dynamic code generation, uses Block trick and local UpValues, so I decided to post it still.

$\endgroup$
12
  • $\begingroup$ How would you solve the withUnits[UnitConvert[0 m/s^2*(1 min)^2, km]] problem? $\endgroup$ Commented Nov 28, 2012 at 20:24
  • $\begingroup$ @nikie I would add this: Quantity /: Times[0, Quantity[_, unit_]] := Quantity[0, unit];. Not sure it will always work though. $\endgroup$ Commented Nov 28, 2012 at 20:29
  • $\begingroup$ Neat trick (+1), but if it's only used with UnitConvert, wouldn't it be easier to just write a new myUnitConvert function instead and substitute it for UnitConvert? In other words, what's the advantage of withUnit here? I don't have version 9, so maybe I'm overlooking something. (BTW I definitely think online access shouldn't be needed for this). $\endgroup$
    – Jens
    Commented Nov 28, 2012 at 21:02
  • $\begingroup$ @Jens Thanks. The advantage is that this allows you to piggyback on the built-in UnitConvert without knowing how it operates, meaning resusing the higher-level abstraction, and that is always better. In essence, my code just generates the same exact code that would be otherwise written by hand, so it can be interpreted as a custom preprocessor written on the level of already parsed code (a macro). $\endgroup$ Commented Nov 28, 2012 at 21:07
  • $\begingroup$ @Jens Perhaps, let me clarify: the code is being generated inside the Block, but then UnitConvert fires normally - if it is present in code. $\endgroup$ Commented Nov 28, 2012 at 21:09
11
$\begingroup$

As noted in the documentation for Quantity, you can use ctrl-= to input units. This uses Wolfram|Alpha, so needs an internet connection.

enter image description here

Quantity will also use Wolfram|Alpha to try to interpret strings, so you could also use:

In[8]:= UnitConvert[Quantity["1 m/s^2*(1 min)^2"], Quantity["km"]]

Out[8]= Quantity[18/5, "Kilometers"]
$\endgroup$
3
  • 7
    $\begingroup$ I've tried that first, but I can't imagine entering long formulas with that. $\endgroup$ Commented Nov 28, 2012 at 20:21
  • $\begingroup$ @nikie Why? You only need 1 Ctrl= call more than the characters you're already typing in your preferred version... If you click the lower box in the above, it'll display the formula exactly as you want in the question. $\endgroup$
    – rm -rf
    Commented Nov 28, 2012 at 20:42
  • $\begingroup$ @rm-rf WolframAlpha inside Mathematica is really slow for me. $\endgroup$
    – Tyilo
    Commented Aug 27, 2013 at 20:35
11
$\begingroup$

You could set an input alias such as

With[{rules = {"m" -> "Meters", "km" -> "Kilometers"}},
 AppendTo[CurrentValue[InputNotebook[], InputAliases], 
  "qu" -> TemplateBox[{"\[SelectionPlaceholder]", "\[Placeholder]"}, 
    "QuantityUnit", 
    DisplayFunction -> (PanelBox[RowBox[{##}], FrameMargins -> 2] &), 
    InterpretationFunction -> (ToBoxes@
        Quantity[ToExpression@#1, Evaluate[#2 /. rules]] &)]]]

Then, escquesc brings up a little panel with a couple of placeholders where you write, for example

Mathematica graphics

You could set the styles on a stylesheet, add units to your list of rules, or even not add any. If it doesn't recognize it, it queries WolframAlpha just like Quantity, and then caches the result. Setting the input alias to $FrontEnd instead of InputNotebook[] would make it global and permanent. Or you could add it to the Notebook style of some stylesheet instead

$\endgroup$
4
  • $\begingroup$ Thanks for the typos and neat keys edits. But why is Evaluate necessary? $\endgroup$
    – Rojo
    Commented Dec 4, 2012 at 15:14
  • $\begingroup$ Because Quantity has the attribute HoldRest, so code like Quantity[5, "m" /. "m" -> "Meters"] remains unevaluated. Maybe you had a couple of copies of the InputAlias in your test notebook, so you didn't notice the problems... $\endgroup$
    – Simon
    Commented Dec 5, 2012 at 0:55
  • $\begingroup$ @Simon, I could swear Quantity didn't have that attribute earlier. I had probably been messing with it $\endgroup$
    – Rojo
    Commented Dec 5, 2012 at 1:01
  • $\begingroup$ @Simon!!! After the first TemporalData use, it's attributes get updated to a simple NHoldAll! $\endgroup$
    – Rojo
    Commented Dec 5, 2012 at 4:08
8
$\begingroup$

For those still preferring the use of AutomaticUnits, Jon has posted a work around to allow its use with v.9 at http://blog.wolfram.com/2010/12/09/automatic-physical-units-in-mathematica/#comments

$\endgroup$
7
$\begingroup$

Here's my extensions to Rojo's answer. I've moved the replacement rules into a global variable, to make them easier to modify on the fly

$UnitReplacementRules = {"fm"->"Femtometers","nm"->"Nanometers","\[Mu]m"->"Micrometers","mm"->"Millimeters","cm"->"Centimeters","m"->"Meters","km"|"kms"->"Kilometers","mi"->"Miles", "s"|"sec"->"Seconds","min"|"mins"->"Minutes","h"|"hr"|"hrs"->"Hours","yr"|"yrs"->"Years", "\[Mu]g"->"Micrograms","mg"->"Milligrams","g"->"Grams","kg"->"Kilograms", "L"|"l"->"Liters","mL"|"ml"->"Milliliters", "N" -> "Newtons", "K"->"Kelvins","mol"->"Moles","M"->"Molar"}

and I've made it cope with compound units with (hopefully) arbitrary 2d structures (e.g., powers and fractions). It also overwrites any existing qu InputAliases

CurrentValue[InputNotebook[], InputAliases] = 
 Append[DeleteCases[CurrentValue[InputNotebook[], InputAliases], "qu" -> _],
  "qu" -> TemplateBox[{"\[SelectionPlaceholder]", "\[Placeholder]"}, 
    "QuantityUnit", Tooltip -> "Unit Template", 
    DisplayFunction -> (PanelBox[RowBox[{#1, StyleBox[#2, "QuantityUnitTraditionalLabel"]}], FrameMargins -> 2] &), 
    InterpretationFunction -> (With[{unit = #2 /. s_String?LetterQ :> "\""~~(s/.$UnitReplacementRules)~~"\""},
        (*Print[{#2, unit, StringTake[ToString[MakeExpression@#2, InputForm], {14, -2}], KnownUnitQ @@ MakeExpression@unit}];*)
        If[KnownUnitQ@@MakeExpression@unit,
         RowBox[{"Quantity", "[", #1, ",", unit, "]"}],
         RowBox[{"Quantity", "[", #1, ",", "\""~~StringTake[ToString[MakeExpression@#2, InputForm], {14, -2}]~~"\"", "]"}]
        ]] &)]]

Some examples: Examples

The same comments as in Rojo's answer apply. This is currently set up to add the alias to only the InputNotebook, it could be made to add the alias to $FrontEnd which would make it work in all notebooks (until it is removed). Note that $UnitReplacementRules would have to be added to the init.m or something similar if you want this code to work across sessions.

$\endgroup$
5
  • $\begingroup$ I just wanted to add a couple of things to the unit handling, so came here to check for new answers. Glad to find you've worked on it. $\endgroup$
    – Rojo
    Commented Dec 16, 2012 at 5:52
  • $\begingroup$ I'll probably take your version, move the DisplayFunction and the InputAlias to a stylesheet, add some way (no idea how yet) to make the "$UnitReplacementRules" remember for ever every conversion made with WolframAlpha. And perhaps if I use it often, make it a shortcut intead of an input alias $\endgroup$
    – Rojo
    Commented Dec 16, 2012 at 6:08
  • $\begingroup$ Any other ideas of improvement? Btw, it is not necessarily better but using Prepend instead of Append I think would effectively override the previous InputAlias without actually deleting it $\endgroup$
    – Rojo
    Commented Dec 16, 2012 at 6:11
  • $\begingroup$ Those are all good ideas. I think another thing to do would be to make a palette to quickly view and modify the $UnitReplacementRules. See the Grassmann Algebra Package's palette for something similar. $\endgroup$
    – Simon
    Commented Dec 20, 2012 at 3:35
  • $\begingroup$ @Simon I have greatly improved the number of units supported with this without WolframAlpha. See my answer :). $\endgroup$
    – Tyilo
    Commented Aug 18, 2013 at 4:42
6
$\begingroup$

Improved version of Simon's answer which tries several different transformation of the input string to convert it to the full form that Mathematica needs:

Begin["System`"]

(* Deca is intentionally left out as only one character prefixes are supported *)
$SIPrefixes={"Y"->"Yotta","Z"->"Zetta","E"->"Exa","P"->"Peta","T"->"Tera","G"->"Giga","M"->"Mega","k"->"Kilo","h"->"Hecto","d"->"Deci","c"->"Centi","m"->"Milli","\[Mu]"|"\[Micro]"->"Micro","n"->"Nano","p"->"Pico","f"->"Femto","a"->"Atto","z"->"Zepto","y"->"Yocto"};
$UnitAbbreviations={"\[Degree]"->"angularDegrees","\[Degree]C"->"degreesCelsius","\[CapitalOmega]"->"ohms","A"->"amperes","Bq"->"becquerels","C"->"coulombs","F"->"farads","Gy"->"grays","H"->"henries","Hz"->"hertz","J"->"joules","K"->"kelvins","L"->"liters","M"->"molar","N"->"newtons","Pa"->"pascals","S"->"siemens","Sv"->"sieverts","T"->"teslas","V"->"volts","W"->"watts","Wb"->"webers","a"->"julianYears","atm"->"atmospheres","au"->"astronomicalUnit","bar"->"bars","cd"->"candelas","d"->"days","eV"->"electronvolts","g"->"grams","h"->"hours","kat"->"katals","lm"->"lumens","lx"->"lux","m"->"meters","min"->"minutes","mol"->"moles","rad"->"radians","s"->"seconds","sr"->"steradians"};

FirstDropWhile[list_, cond_] := (
    l = LengthWhile[list,cond];
    If[l == Length[list],
        None,
        list[[l+1]]
    ])
StringCapitalize[str_] := ToUpperCase @ Characters[str][[1]] <> StringDrop[str, 1]
ReplaceUnit[str_] := str /. $UnitAbbreviations
    ReplaceSIPrefix[str_] := (Characters[str][[1]] /. $SIPrefixes) <> StringDrop[str, 1]

UnitFullName[str_]:=(
    transformations = {Identity, StringCapitalize,
        Composition[StringCapitalize,ReplaceUnit], ReplaceSIPrefix,
        (ReplaceSIPrefix@Characters[#][[1]]) <> ReplaceUnit[StringDrop[#,1]]&
    };
    candidates = Flatten[{#, # <> "s"}& /@ Through[transformations[str]]];
    FirstDropWhile[candidates, !KnownUnitQ@# &]
)

CurrentValue[$FrontEnd, InputAliases] = 
    	Append[DeleteCases[CurrentValue[$FrontEnd, InputAliases], "qu" -> _],
    "qu" -> TemplateBox[{"\[SelectionPlaceholder]", "\[Placeholder]"}, 
    "QuantityUnit", Tooltip -> "Unit Template", 
    DisplayFunction -> (PanelBox[RowBox[{#1, StyleBox[#2, "QuantityUnitTraditionalLabel"]}], FrameMargins -> 2] &), 
    InterpretationFunction -> (With[{unit = #2 /. s_String?LetterQ :> "\""~~(UnitFullName[s])~~"\"" /. s_String :> (s /. "\[CenterDot]" -> "*")},
        If[KnownUnitQ@@MakeExpression@unit,
            RowBox[{"Quantity", "[", #1, ",", unit, "]"}],
            RowBox[{"Quantity", "[", #1, ",", "\""~~StringTake[ToString[MakeExpression@#2, InputForm], {14, -2}]~~"\"", "]"}]
        ]] &)]]

End[]

Sorry for the lack of whitespace. I made this using TraditionalForm.

$\endgroup$
5
$\begingroup$

I gave up trying to use Quantity and the units package in version 9 because of the drop in speed and the clumsy way of incorporating units that makes long expressions difficult to read. I have therefore stuck with Jon McLoone's package that works very well. It can found at http://blog.wolfram.com/2010/12/09/automatic-physical-units-in-mathematica/#comments

$\endgroup$
5
$\begingroup$

I fully agree that the current Units syntax is cumbersome. I thus found this thread, which I came upon recently, of great interest.

The various code blocks in the previous answers each offered unique functionality at the time they were written. Alas, they are currently all either non-canonical, or cause the current version of MMA to crash.

Consider, for instance, the following, which contains a variable for the quantity magnitude:

UnitConvert[Quantity[z , "Minutes"], "Seconds"] 

60 z s

Neither the myUnits package nor the withUnits module can do the above calculation:

UnitConvert[z minute, second]  (*following activation of myUnits*)

UnitConvert[z (1 min), 1 s]

withUnits[UnitConvert[z min, s]] (*following activation of withUnits*)

UnitConvert[z (1 min), 1 s]

An additional limitation is that these only work for the particular units or physical constants you've pre-defined. [I should add that both of these limitations were effectively noted by the author of withUnits, Leonid Shiffrin, who pointed out that his code was offered as a prototype that "will only be as good as you make it to be".] Thus I would say the best application of these two code blocks would be for "production work" (entering many formulas using a known set of units, in a known way), where their speed and simplicity of use would offer great convenience, and you'd be controlling for potential non-canonical behavior by using them under well-tested circumstances.

My typical use case is, however, different — I most commonly do one-offs, in which I may need to use a unit or physical constant I've never used before, or in a way I've never before used it.
Ctrl= is more suitable for that application, and doesn't fail for the above example, but I find it a bit tedious for long formulas. For instance, even with the above simple formula, to enter "min" I have to hit Ctrl=, then type "minutes", then hit the right arrow to get out of the box, then wait for the Wolfram server to respond; only after all this can I enter the comma. This then has to be repeated to enter "s".

Another inconvenience with Ctrl= is that you have to change any variable name that Wolfram Alpha interprets as a unit, no matter how obscure. For instance, you can't use Ctrl= to input "wl seconds" (where wl is a variable), since WA interprets that as "wekoliters seconds". [You can enter wl outside the Ctrl= statement, but that puts the variable outside the quantity statement, which is not proper syntax and can result in unexpected behavior.]

Having said that, Ctrl= is very handy for unit discovery.

Finally, all three versions of the code blocks that create unit-entry-panels cause my version of MMA (11.3 running on MacOS 10.13.2) to crash as soon as I type esc q.

Thus, to add one additional alternative to the mix, I'll offer the very basic approach I use—I simply handle Quantity's cumbersome syntax by creating keyboard shortcuts:

I've set Ctrlq to give:

 Quantity[, ""]

And Ctrlu to give:

 UnitConvert[Quantity[, ""], ""]

Thus, anytime I need to use Quantity, I hit Ctrlq, and fill in the rest. I'd describe this as a compromise that partly addresses Quantity's syntactical inconvenience, while remaining completely canonical. With formulas requiring many Quantity commands, it makes things go much faster. [Warning: I create the above shortcuts with Keyboard Maestro, which works fine. However, for additional convenience, you might be tempted to get fancier and make use of Keyboard Maestro's ability to place the cursor at a particular point within the pasted command. Don't do that; it causes unexpected behavior.]

Alternatively, if you want a "production" approach (with the limitations and caveats that entails), you might try the following. I've not yet found any non-canonical behavior with this approach, but of course there could be corner cases I've not yet encountered:

m[uqty_] := Quantity[uqty, "Meters"]
min[uqty_] := Quantity[uqty, "Minutes"]
s[uqty_] := Quantity[uqty, "Seconds"]
k[uqty_] := Quantity[uqty, "Kelvins"]
um[uqty_] := Quantity[uqty, "Micrometers"]
(*etc....*)

Units can then be entered with either Postfix (to ensure reliable operation each quantity-unit combination should be enclosed in parentheses)...

UnitConvert[(z // min), (1 // s)]

60 z s

...or Prefix (less typing, but requires entering the quantity after the unit, which may take some getting used to):

UnitConvert[min@z, s@1]

60 z s

Note that to just enter, say, "Seconds", it is necessary to include a "1", i.e., "(1//s)" or "s@1". Note also that it doesn't accept composite units; units must be entered separately, e.g.:

Integrate[(9.8 // m)/(1 // s)^2 *t, {t, (0 // s), (10 // s)}]    
Integrate[[email protected]/(s@1)^2 *t, {t, s@0, s@10}]

490. m
490. m

It also works with FormulaData:

FormulaData[{"PlanckRadiationLaw", "Wavelength"}, 
{"T" -> k@5000, "\[Lambda]" -> um@\[Lambda]}]

$\lambda \neq 0\land \text{L}(\lambda )=\frac{1.19104\times 10^{14}\text{Pa}/\text{s}}{2.71828^{2.87755/\lambda } \lambda ^5-1. \lambda ^5}$

$\endgroup$

Your Answer

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

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