Getting started with FlexUnits.jl

I am interested in using the FlexUnits.jl package. I have read through the documentation, but I’m not clear on the best way to deal with the UnitRegistry.

When loading the package, I want to …

  1. load typical Imperial and Metric units (u"psi")
  2. set constants for my most used (const psi = u"psi")
  3. set my preferred unit simplifications (set_preferred_unit(psi))

Where is the best place to keep this personalization code?

It seems to me the simplest for usage would my own MyFlexUnits package that has @reexport using FlexUnits and all the customizations. However, that workflow isn’t mentioned in the docs. The docs seem to want me to keep my customization code in a standalone file/module, but then where do I store it, and how do I load it?

Moreover, the docs say you can either modify the included unit registry or create your own. If I need to create my own package or file anyway, I wonder if there is much of a difference between using a bunch of modifying function calls in a row like register_unit versus just copy-paste-editing the default unit registry.

Workflow suggestions from people already using the package would be appreciated!

the wrapper-package-with-reexport approach works, but its heavier than you need. the customization is really just session setup, so id keep it lighter.

what id do: a small local module, a single file you include (or a devved package if you want it versioned), that does using FlexUnits, .UnitRegistry, then your register_unit calls, your consts, and set_preferred_unit. load it from startup.jl or include it at the top of scripts.

two things worth knowing. first, register_unit mutates a PermanentDict at runtime, so those calls have to run every session, and you cant change a value once its set in a session. thats the real reason the customization lives in code that runs at load, not in a file you edit once.

second, on register_unit calls vs copy-paste-editing the default registry: the calls compose with package updates, you keep getting upstream registry changes for free. a copied registry file is a fork you now own, including every future merge. unless you need different dimension or transform types (then use the UnitRegistry.jl template with registry_defaults! plus your additions), prefer the calls.

I don’t want to have to keep track of this file when I move computers.


I have a wrapper package together now. The units themselves are working, but the automatic simplifications are not.

module VesselUnits

# Names exported from this package
export inch, mm, lb, kg, lbf, °F, °C, psi, MPa, kPa, bar, atm, STEEL_DENSITY

# Load and reexport dependencies
using Reexport
@reexport using FlexUnits
@reexport using .UnitRegistry

# Add units
register_unit("atm" => 101.325 * u"kPa")

# Define selected units in namespace
const inch = u"inch" # Imperial Length
const mm = u"mm"     # Metric Length
const lb = u"lb"     # Imperial Mass
const kg = u"kg"     # Metric Mass
const lbf = u"lbf"   # Imperial Force
# N is too common    # Metric Force
const °F = u"°F"     # Imperial Temperature
const °C = u"°C"     # Metric Temperature
const psi = u"psi"   # Imperial Pressure
const MPa = u"MPa"   # Metric Pressure
const kPa = u"kPa"   # Alternate Metric Pressure
const bar = u"bar"   # Alternate Metric Pressure
const atm = u"atm"   # Alternate Metric Pressure

# Define important constants in namespace
const STEEL_DENSITY = 0.28lb/inch^3;

# Set preferred units for simplification
set_preferred_unit(inch)
set_preferred_unit(lb)
set_preferred_unit(lbf)
#set_preferred_unit(°F)  # ERROR: NotScalarError: °F cannot be treated as scalar, operation only valid for scalar units
set_preferred_unit(psi)
set_preferred_unit(lb/inch^3)

display_simplified_units(true)  # Always convert to simple preferred units

end
julia> using VesselUnits

julia> STEEL_DENSITY * 1inch^3  # I want `0.28lb`
0.12700576000000002 kg

julia> 3lbf / 1inch^2  # I want `3psi`
20684.264368528737 kg/(m s²)

julia> display_simplified_units(true)  # I already have this line in my package, but I needed it again in my REPL.
true

julia> 3lbf / 1inch^2  # Still not in `psi`
20684.264368528737 Pa

julia> set_preferred_unit(psi)  # I already have this line in my package, but I needed it again in my REPL.
psi

julia> 3lbf / 1inch^2  # Now it's correct units, but only single precision?
2.9999977328476604 psi

I’m also not able to set a preferred affine temperature unit. This should be possible since preferred units are just for displaying results right? I think the underlying calculations are all done in SI units.

julia> set_preferred_unit(u"°F")
ERROR: NotScalarError: °F cannot be treated as scalar, operation only valid for scalar units
Stacktrace:
 [1] assert_scalar
   @ C:\Users\nboyer\.julia\packages\FlexUnits\IIDLu\src\types.jl:703 [inlined]
 [2] set_preferred_unit!(unitset::Vector{Units{…}}, u::Units{StaticDims{…}, AffineTransform{…}}; warn_failure::Bool)
   @ FlexUnits C:\Users\nboyer\.julia\packages\FlexUnits\IIDLu\src\utils.jl:234
 [3] set_preferred_unit!
   @ C:\Users\nboyer\.julia\packages\FlexUnits\IIDLu\src\utils.jl:233 [inlined]
 [4] set_preferred_unit(u::Units{StaticDims{K}, AffineTransform{Float64}})
   @ FlexUnits C:\Users\nboyer\.julia\packages\FlexUnits\IIDLu\src\utils.jl:221
 [5] top-level scope
   @ REPL[7]:1
Some type information was truncated. Use `show(err)` to see complete types.

If you are going to do that much work on a unit registry, I would just go out and create a unit registry myself as shown in the docs here. The way I would try to modify what you did is as follows:

module VesselUnits

# Names exported from this package
export inch, mm, lb, kg, lbf, °F, °C, psi, MPa, kPa, bar, atm, STEEL_DENSITY

# Use registry tools for convenience functions
using FlexUnits.RegistryTools
import ..set_preferred_unit, ..display_simplified_units


# Define your unit registry as a dictionary
const UNITS = PermanentDict{Symbol,Units{Dimensions{FixRat32},AffineTransform{Float64}}}()

# Add units and register new ones directly to UNITS
registry_defaults!(UNITS)
register_unit!(UNITS, "atm" => 101.325 * UNITS[:kPa])

# Define your preferred units directly as a vector (replace with what you want)
const PREFERRED_UNITS = [UNITS[u] for u in [:F, :H, :T, :Ω, :V, :W, :J, :Pa, :N, :C, :L]]

# Use the macro to create your preferred units and string macros
@generate_unit_simplifier(PREFERRED_UNITS)
@generate_registry_exports(UNITS)

# Now that macros are generated, define selected units in namespace
const inch = u"inch" # Imperial Length
const mm = u"mm"     # Metric Length
const lb = u"lb"     # Imperial Mass
const kg = u"kg"     # Metric Mass
const lbf = u"lbf"   # Imperial Force
# N is too common    # Metric Force
const °F = u"°F"     # Imperial Temperature
const °C = u"°C"     # Metric Temperature
const psi = u"psi"   # Imperial Pressure
const MPa = u"MPa"   # Metric Pressure
const kPa = u"kPa"   # Alternate Metric Pressure
const bar = u"bar"   # Alternate Metric Pressure
const atm = u"atm"   # Alternate Metric Pressure

# Define important constants in namespace
const STEEL_DENSITY = 0.28lb/inch^3

# Set preferred units for simplification (can also do this when defining PREFERRED_UNITS)
set_preferred_unit(inch)
set_preferred_unit(lb)
set_preferred_unit(lbf)
set_preferred_unit(psi)
set_preferred_unit(lb/inch^3)

#This sets the global constant to simplify units when displaying results (needs to be imported)
display_simplified_units(true)

end

Now it does what you want

julia> using .VesselUnits 

julia> STEEL_DENSITY * 1inch^3
0.28 lb

julia> 3lbf / 1inch^2
2.9999977328476604 psi

Unfortunately, you can’t set affine units as a preferred unit, because compound affine units actually doesn’t make sense at a fundamental level. For example, people write heat capacities as “btu/(lb*°F)” but you never divide or multiply by “°F” directly. You divide or multiply by a difference which implicitly converts your temperatures to “°Ra”. So if you’re being pedantically correct (which computers require you to be), heat capacity is actually “btu/(lb*°Ra)”. You actually cannot even call u"J/(lb*°F)" (btu isn’t registered by default because there’s different definitions) because the operation isn’t allowed on affine units, so you cannot simplify to something like this.

I have been thinking about allowing implicit conversion of expressions like btu/(lb*°F) to btu/(lb*°Ra). I can’t think of a case where any combined affine unit didn’t imply a scalar unit. I’m only currently disallowing this out of an abundance of caution, and the fact that other unit packages are even more strict about affine units. However, I already automatically convert to Kelvin when doing something like

julia> dHdT = 4.184u"kJ/(kg)" * 1.0u"kg" / (5u"°C") # Probably not the result you want
15.042243393852239 (m² kg)/(s² K)

julia> dHdT = 4.184u"kJ/(kg)" * 1.0u"kg" / (5u"°C" - 0u"°C") # Probably the result you want
836.8 (m² kg)/(s² K)

If I’m already assuming we’re competent people with respect to affine units, I should probably allow compound affine units in expressions too.

I didn’t find documentation on what is supposed to be in PREFERRED_UNITS. What dimensions need to be included in what order? I could guess at some of these units, but I want to be sure.

Does setting lb and inch in PREFERRED_UNITS imply my preference for density is lb/inch^3? set_preferred_unit(lb/inch^3) didn’t throw any errors, but lb/inch^3is not a key in UNITS.


What is the name of the constant that needs to be imported?


I know I’m not going to get exactly 3.0 back, but why are there only five 9’s in this result?

In general, I am having trouble figuring out how to use these tools outside of the one specific “Advanced Example” that is more advanced than I need.

help?> UnitRegistry.UNITS
  No documentation found for private binding FlexUnits.UnitRegistry.UNITS.

help?> UnitRegistry.PREFERRED_UNITS
  No documentation found for private binding FlexUnits.UnitRegistry.PREFERRED_UNITS.

help?> @generate_unit_simplifier
  No documentation found for public binding FlexUnits.RegistryTools.@generate_unit_simplifier.

help?> @generate_registry_exports
  No documentation found for public binding FlexUnits.RegistryTools.@generate_registry_exports.

help?> set_preferred_unit
  No documentation found for public binding FlexUnits.set_preferred_unit.

help?> display_simplified_units
  No documentation found for public binding FlexUnits.display_simplified_units.

Sorry about that. Unit simplification is a brand new feature that I haven’t had time to document yet. It’s turned out to be a bit more popular than I expected. I’ll try to document it more this weekend. As for ordering PREFERRED_UNITS, if you call the @generate_unit_simplifier macro, then set_preferred_unit will order the symbols properly for you (the algorithm is greedy so it tries to resolve the most “complicated” units first). The

You can put any unit in PREFERRED_UNITS, the stimulation ruling out affine units has been removed. Compound units are allowed in “PREFERRED_UNTS”, and are easy to add once set_preferred_unit has been activated via the macro. There is an internal algorithm that tries to detect compound units and put brackets around them before doing things like squaring.

I still don’t have preferred unit simplification working based on your example.

julia> using VesselUnits

julia> STEEL_DENSITY * 1inch^3
0.12700586360000002 kg

julia> 3lbf / 1inch^2
20684.271879505082 kg/(m s²)
module VesselUnits

# Export names from this package
export inch, mm, lb, kg, lbf, °F, °C, psi, MPa, kPa, bar, atm
export STEEL_DENSITY
export simplify, set_preferred_unit

# Load dependencies
using FlexUnits.RegistryTools
using FlexUnits: set_preferred_unit, display_simplified_units, simplify

# Define the unit registry as an empty dictionary
const UNITS = PermanentDict{Symbol,Units{Dimensions{FixRat32},AffineTransform{Float64}}}()

# Add default units and register new ones to UNITS dictionary
registry_defaults!(UNITS)
register_unit!(UNITS, "atm" => 101.325 * UNITS[:kPa])

# Define preferred units
# const PREFERRED_UNITS = [UNITS[u] for u in [:F, :H, :T, :Ω, :V, :W, :J, :Pa, :N, :C, :L]]
const PREFERRED_UNITS = [UNITS[u] for u in [:inch, :lb, :lbf, :Ra, :psi]]  # Doesn't seem to do anything??

# Generate simplifiers and exports for defined units with included macros
@generate_unit_simplifier(PREFERRED_UNITS)
@generate_registry_exports(UNITS)

# Define selected units in namespace
const inch = u"inch" # Imperial Length
const mm = u"mm"     # Metric Length
const lb = u"lb"     # Imperial Mass
const kg = u"kg"     # Metric Mass
const lbf = u"lbf"   # Imperial Force
# N is too common    # Metric Force
const °F = u"°F"     # Imperial Temperature
const Ra = u"Ra"     # Alternate Imperial Temperature
const °C = u"°C"     # Metric Temperature
const psi = u"psi"   # Imperial Pressure
const MPa = u"MPa"   # Metric Pressure
const kPa = u"kPa"   # Alternate Metric Pressure
const bar = u"bar"   # Alternate Metric Pressure
const atm = u"atm"   # Alternate Metric Pressure

# Define important constants in namespace
const STEEL_DENSITY = 0.28lb/inch^3;

# Set preferred units for simplification (Why do I still need these?)
set_preferred_unit(inch)
set_preferred_unit(lb)
set_preferred_unit(lbf)
set_preferred_unit(Ra)  # Too dangerous to leave on °F. Temperature differences will be wrong.
set_preferred_unit(psi)
set_preferred_unit(lb/inch^3)

display_simplified_units(true)  # Always convert to simple preferred units (Doesn't work.)

end  # module

Wait a minute, did you forget to put a “.” in front of VesselUnits? I literally copied and pasted your module and it worked. Using local modules properly requires a dot in front of it. This isn’t a FlexUnits thing, this is a Julia thing (and also a Python thing).

using .VesselUnits

julia> STEEL_DENSITY * 1inch^3
0.28 lb

julia> 3lbf / 1inch^2
3.0 psi

By the way, I would try to keep the default PREFERRED_UNITS and just use set_preferred_unit to replace the the appropriate defaults. It seems like you and I are both mechanical/process engineers but some poor electrical engineer might be faced with some horrendous looking units if the defaults are overridden. Electrical units are the worst (I made unit simplification mostly for my electrical simulation work).

As for how modules work, look at the link below (it confused me for a while too).

https://stackoverflow.com/questions/77289762/when-do-i-have-to-specify-the-imported-module-using-using-to-use-a-function

My module is a package. You can load it from here. I am testing it in its own environment currently and getting the behavior above.

(VesselUnits) pkg> st
Project VesselUnits v0.0.1
Status `C:\Users\nboyer\Documents\Julia\dev\VesselUnits\Project.toml`
  [76e01b6b] FlexUnits v0.6.9

Weirdly though, if I run it as a local module as you show, it does work.

julia> pwd()
"C:\\Users\\nboyer\\Documents\\Julia\\dev\\VesselUnits"

julia> include("src/VesselUnits.jl")
Main.VesselUnits

julia> using .VesselUnits

julia> STEEL_DENSITY * 1inch^3
0.28 lb

julia> 3lbf / 1inch^2
3.0 psi

I changed this because you said

so having both seemed redundant. But I am still not clear on what all the default units shown there are, and thus what needs to be changed vs. appended. (And also, if length and order of the elements in PREFERRED_UNITS matter.)

The length of PREFERRED_UNITS doesn’t matter, but order does because the algorithm is greedy. The most complex units must come first. However, order gets fixed when complexity_sort! is called (which is done in the macro) so order doesn’t matter as long as you call the @generate_unit_simplifier macro or complexity_sort!.

As for creating a package, FlexUnits expects unit registries to be a modules in and of themselves; this makes exports cleaner. Anything used by UnitRegistryTools should only be used inside a unit registry module (in order to prevent polluting namespaces). Uniful.jl and DynamicQuantities.jl work in similar manners (string macros in Unitful.jl actually searche for units in multiple modules). Maybe you should put our code inside a local module called VesselUnitRegistry add

using .VesselUnitRegistry

inside your package, and then export all the stuff you want from VesselUnits.jl.

I’m not getting it. Auto simplification still isn’t working with a separate submodule. I’m not really trying to create a replacement package; I just want to use FlexUnits.jl with a portable set of preferred units and constants. See my new branch for the current attempt.

Hmmm… that’s weird. This may be one of those weird times where you need an __init__() method in order to ensure a setting change (like turning on auto simplification) is enforced at load time. I’ve got some time today, I’ll see what I can do.

Stepping back a bit though, if you want to use a module in code, you need to include it and then call it with a dot. I use “portable modules” all the time, and this is the easiest way to get that pattern to work consistently.

include("MyRegistryModule.jl")
using .MyRegistryModule

This is how FlexUnits.jl was intended to be used. I might want to use the “include” in my tutorials but the dot calls are explicitly there.

Packages are a completely different beast. What you have in your repo is a bona fide package. If that’s the case, then you will likely need to add an __init__() method. It’s not a FlexUnits thing, it’s definitely a Julia thing. I’ll take a look at your code and see how I can help. I really should work through this and add it to my tutorials.

I would appreciate that!

Copying that module file between computers/projects and getting the correct relative path is definitely not easier than:

] add https://github.com/nathanrboyer/VesselUnits.jl
using VesselUnits

Especially if you need to update all of the copies in the future.

I thought packages were just fancy modules, so I didn’t expect using them to work so differently here.

The __init__() function was the ticket to forcing the appropriate settings. Note that Unitful also requires this if you’re extending it.

julia> using VesselUnits

julia> STEEL_DENSITY * 1inch^3
0.28 lb

julia> 3lbf / 1inch^2
0.003 ksi

I forked your repo and this is what I used.

"""
    Package VesselUnits v$(pkgversion(VesselUnits))

Customized version of FlexUnits.jl for pressure vessel development.
"""
module VesselUnits

import FlexUnits: set_preferred_unit, simplify, display_simplified_units

# Export names from this package
export inch, mm, lb, kg, lbf, °F, °C, psi, ksi, MPa, kPa, bar, atm
export STEEL_DENSITY
export simplify, set_preferred_unit

# Export common macros and functions from internal registry (this package is opinionated)
export @u_str, @ud_str, @q_str, @D_str, uparse, qparse, register_unit

# Build internal unit registry with default settings
module InternalRegistry
    using FlexUnits.RegistryTools

    # Define the unit registry as an empty dictionary
    const UNITS = PermanentDict{Symbol,Units{Dimensions{FixRat32},AffineTransform{Float64}}}()

    # Add default units and register new ones to UNITS dictionary
    registry_defaults!(UNITS)
    register_unit!(UNITS, "kip" => 1000 * UNITS[:lbf])
    register_unit!(UNITS, "ksi" => 1000 * UNITS[:psi])
    register_unit!(UNITS, "atm" => 101.325 * UNITS[:kPa])
    register_unit!(UNITS, "mph" => UNITS[:mi] / UNITS[:hr])

    # Define preferred units
    const PREFERRED_UNITS = [UNITS[u] for u in [:F, :H, :T, :Ω, :V, :W, :J, :Pa, :N, :C, :L]]

    # Generate simplifiers and exports for defined units with included macros
    @generate_unit_simplifier(PREFERRED_UNITS)
    @generate_registry_exports(UNITS)
end 

# Ensure the internal package is "used" 
using .InternalRegistry

# Define selected units in namespace
const inch = u"inch" # Imperial Length
const mm = u"mm"     # Metric Length
const lb = u"lb"     # Imperial Mass
const kg = u"kg"     # Metric Mass
const lbf = u"lbf"   # Imperial Force
# N is too common    # Metric Force
const °F = u"°F"     # Imperial Temperature
const Ra = u"Ra"     # Alternate Imperial Temperature
const °C = u"°C"     # Metric Temperature
const psi = u"psi"   # Imperial Pressure
const ksi = u"ksi"   # Alternate Imperial Pressure
const MPa = u"MPa"   # Metric Pressure
const kPa = u"kPa"   # Alternate Metric Pressure
const bar = u"bar"   # Alternate Metric Pressure
const atm = u"atm"   # Alternate Metric Pressure

# Define important constants in namespace
const STEEL_DENSITY = 0.28lb/inch^3;

# Set preferred units for simplification and turn unit simplification on at startup
function __init__()
    set_preferred_unit(inch)
    set_preferred_unit(lb)
    set_preferred_unit(lbf)
    set_preferred_unit(Ra)  # Too dangerous to leave on °F. Temperature differences will be wrong.
    set_preferred_unit(ksi)
    set_preferred_unit(lb/inch^3)

    display_simplified_units(true)  # Always convert to simple preferred units
end

end  # module

I’ll create a pull request from the fork.

You are ALMOST right. Packages are modules with a list of dependencies, and are compiled by default. This means that when you add a package, code is compiled but not necessarily run. Some code needs to be wrapped in an __init__() method and executed when the package is being loaded in your environment. Also, calling external libraries (such as Python or C libraries) can only be set up in an __init__(). I learned that one the hard way (my very first segfault). I’m not sure about all the design decisions behind this, but it does remove a major attack surface that plagues Python.