Unit Systems¶
A frame stores x = 3.0. Is that 3 Å or 3 nm? The array does not say.
A unit preset names the convention that gives bare numbers meaning, and a
unit registry converts explicitly when two conventions meet (for example
LAMMPS real vs metal). Both are molrs's: mp.core.UnitPreset and
mp.core.UnitRegistry are molrs.core.UnitPreset and molrs.core.UnitRegistry.
What it is not: automatic unit tracking on every Frame column. Numbers stay
plain; you attach a convention when you convert or compare.
Why conventions matter¶
A TIP3P field authored in nanometres expects nm coordinates; an OPLS field in ångström expects Å. Mixing them silently produces wrong physics. MolPy does not guess: the force field and the unit preset you choose fix the interpretation.
Using a preset¶
Named presets mirror the LAMMPS units styles, plus openmm:
import molpy as mp
print(mp.core.UnitPreset.names())
# ['cgs', 'electron', 'lj', 'metal', 'micro', 'nano', 'openmm', 'real', 'si']
real = mp.core.UnitPreset("real") # LAMMPS 'real': Å, fs, kcal/mol, amu, e
print(real.length(), real.energy()) # angstrom kilocalorie_per_mole
print(real.boltzmann()) # k_B in kcal/(mol·K)
u = mp.core.UnitRegistry()
length = 3.0 * u.parse(real.length())
print(length.to(u.nanometer)) # 0.3 nanometer
| Preset | Convention |
|---|---|
real |
Å, fs, kcal/mol, amu, e — the OPLS/AMBER default. |
metal |
Å, ps, eV, amu, e. |
si / cgs |
SI / CGS base units. |
electron |
atomic (Hartree) units. |
micro / nano |
micro- and nano-scale presets. |
openmm |
nm, ps, kJ/mol, amu, e — OpenMM's native units (not a LAMMPS units style). |
Defining your own preset¶
Register a custom convention once, then reuse it by name. A preset names one unit for each of its ten dimensions and carries its two constants:
real = mp.core.UnitPreset("real")
dims = ("mass", "length", "time", "energy", "temperature", "charge",
"pressure", "velocity", "force", "density")
units = {dim: getattr(real, dim)() for dim in dims}
mine = mp.core.UnitPreset.register(
"my_units",
{**units, "length": "nanometer"},
boltzmann=real.boltzmann(),
coulomb=real.coulomb(),
)
print(mp.core.UnitPreset("my_units").length()) # nanometer
overwrite=False (the default) refuses to replace an existing preset.
For coarse-grained work, UnitRegistry.define_lj_units(mass, sigma, epsilon)
adds the reduced (Lennard-Jones) units lj_sigma, lj_tau,
lj_epsilon_over_kB, … to a registry from reference Quantity values:
argon = mp.core.UnitRegistry()
argon.define_lj_units(
39.948 * argon.amu, 3.405 * argon.angstrom, 0.2381 * argon.kilocalorie_per_mole
)
print((1.0 * argon.lj_tau).to(argon.ps).magnitude) # ≈ 2.16
Converting quantities¶
Quantities are mp.core.Quantity. Multiply a number by a unit, then .to(...) to
convert; .magnitude reads the bare number. The Boltzmann constant is the
unit k_B:
u = mp.core.UnitRegistry()
e = 2.5 * u.eV
print(e.to("J")) # convert energy (per particle)
print((5 * u.angstrom).to("nm")) # convert length
print(e.magnitude) # 2.5
print((1.0 * u.kilocalorie_per_mole).to("eV").magnitude)
print((1.0 * u.k_B).to("electron_volt / kelvin").magnitude) # 8.617e-05
A unit only one registry knows (an LJ scale, a defined unit) converts with
that registry's unit object: q.to(argon.lj_sigma), or
q.to(argon.parse("lj_sigma")).
Pitfalls¶
- A
Frame's numbers still carry no unit. The registry converts quantities you build; it does not tag your coordinate arrays. Keep your inputs consistent with the force field's convention. - Match the force field. If a field was authored in
real(Å), don't feed it nm coordinates. UnitPreset.register(..., overwrite=False)raises if the name exists — passoverwrite=Truedeliberately.- Not Pint. There is no
pintruntime dependency and no Pint-only context API; unit math is the unit engine.
See also¶
- Naming Conventions — the column schema those unitless arrays follow.
- Force Field — where a convention becomes physical.