Overview
Oneil is a design specification language for rapid, comprehensive system modeling.
Traditional approaches to system engineering are too cumbersome for non-system engineers who don’t have all day. Oneil makes it easy for everyone to contribute to the central source of system knowledge. With Oneil everyone can think like a system engineer and understand how their design impacts the whole.
Oneil enables specification of a system model, which is a collection of parameters, or attributes of the system. The model can be used to evaluate any corresponding design (which is a collection of value assignments for the parameters of the model). In addition, models may have submodels, which are models representing a subsystem.
flowchart TD
design --> model
model --> submodels
model --> plugins
design["`
**Design**
_Overwrites some input parameters with new values_
`"]
model["
<b>Model</b><br>
Capture <i>parameter definitions</i> and <i>relationships between parameters</i><br>
Relationships may depend on <i>parameters imported from submodels</i><br>
Includes a 'default design' with <i>default values for all input parameters</i>
"]
submodels@{shape: docs, label: "
<b>Submodels</b>
Models imported by another model
"}
plugins[["
<b>Plug-ins</b>
Python code that can run numerical simulations
"]]
Here is a quickstart on Oneil syntax. A more in-depth exploration of Oneil can be found in the chapters that follow.
Simple parameter
A basic parameter has the shape Name: identifier = value.
Window count: n_w = 20
Space domain: D_s = 'interstellar'
Here Retry count / Space domain are names, n_retry / D_s are identifiers
(symbols), and 20 / 'interstellar' are values.
Parameters that are directly assigned values are referred to as “independent parameters”.
Limits
Limits can be used to constrain a parameter to a set of allowed values. By default, Oneil allows a parameter to have values from 0 to infinity.
Continuous limits are specified after the name with the syntax (min, max).
Any value between min and max is a valid parameter value.
Battery efficiency (0, 1): eta = 0.90
Azimuth look angle (0, 2*pi): psi = pi
Discrete limits are specified with the syntax [value1, ..., valueN]. Only the
values specified in the limit are valid values.
Battery array configuration ['series', 'parallel']: config = 'series'
Notes
There are times you may want to add more information to a parameter, such as
references, or an explanation of the calculation. To add documentation to a
parameter, use ~ for a single-line note or wrap the text in ~~~ for a
multi-line note.
Notes support inline LaTeX.
Cylinder radius: r = d/2 :km
~ Distance from the center to the inner rim.
Artificial gravity: g_a = r*omega^2 :m/s^2
~~~
The position of a point on the rim of a rotating cylinder is:
$\vec{r}(t) = r\cos(\omega t)\,\hat{i} + r\sin(\omega t)\,\hat{j}$
Taking the first derivative gives the velocity:
$\vec{v}(t) = \frac{d\vec{r}}{dt} = -r\omega\sin(\omega t)\,\hat{i} + r\omega\cos(\omega t)\,\hat{j}$
Taking the second derivative gives the acceleration:
$\vec{a}(t) = \frac{d\vec{v}}{dt} = -r\omega^2\cos(\omega t)\,\hat{i} - r\omega^2\sin(\omega t)\,\hat{j} = -\omega^2\vec{r}(t)$
The acceleration points radially inward (toward the center), and its magnitude is:
$|\vec{a}| = r\omega^2$
This centripetal acceleration acts as artificial gravity for inhabitants
standing on the inner rim of the cylinder, so $g_a = r\omega^2$.
~~~
Units
A defining feature of Oneil is that it ensures that unit arithmetic is correct, and it performs automatic conversions when needed.
To assign a unit to an independent variable, add :<unit> to the parameter.
Earth's gravity: g_E = 9.80664 :m/s^2
Earth rotation period: T_E = 23.9344696 :hr
Note that limits are always assumed to be in terms of the parameter’s unit.
Servo position (0, 360): p = 180 :deg
Intervals
Oneil also allows a parameter to represent a range of values, known as an
interval. Intervals are represented using the syntax min | max.
Ambient temperature: t = 249 | 305 :K
Battery efficiency: eta = 0.8 | 0.9
To get the minimum and maximum value of an interval, use the min and max
functions.
Max ambient temperature: t_max = max(t)
Min battery efficiency: eta_min = min(eta)
Comments
Comments in Oneil are the same as comments in Python. They start with a # and
go until the end of the line. Comments are ignored by Oneil
# TODO: verify that this number is accurate
Satellite mass: m_sat = 0.5 :kg
Dependent parameters
A parameter can reference other parameters in the model. This is referred to as a dependent parameter.
Earth's gravity: g_E = 9.81 :m/s^2
Rocket mass: m = 2e6 :kg
Minimum thrust required: thrust_min = m * g_E :N
Dependent parameters can also use intervals.
Power consumption: P_c = eta_c * P_q | eta_c * P_a
# or, more simply
Power consumption: P_c = eta_c * (P_q | P_a)
Tests
Use tests to verify that a requirement is met.
Body length: l_body = 0.25 :m
Antenna length: l_ant = 0.1 :m
Maximum length: l_max = 0.5 :m
test: l_body + l_ant <= l_max
Tests can also be annotated with notes.
Earth's gravity: g_E = 9.81 :m/s^2
Artificial gravity: g_a = 9.79 :m/s^2
test: g_E*0.9 <= g_a <= g_E*1.1
~ Artificial gravity should be within 10% of Earth's gravity
Importing Python
Oneil allows users to import python code so that models can perform repetitive calculations or more complex calculations such as simulations. Import python code by using the syntax import <python_file> where <python_file> is the path to the python file without .py, relative to the model file. The path may include folders (import ../functions, import lib/helpers), like submodel paths. Functions from that file can then be used in expressions.
import sphere_math
Earth radius: r_E = 6371 :km
Earth surface area: A_E = sphere_surface_area(r_E) :km^2
Earth volume: V_E = sphere_volume(r_E) :km^3
# sphere_math.py
import math
def surface_area(radius):
"""Return surface area of a sphere."""
return 4 * math.pi * radius_km ** 2
def volume(radius):
"""Return volume of a sphere"""
return (4/3) * math.pi * radius_km ** 3
Units are handled automatically by mathematic operators in Python.
Fallback Operator
When a python function may error, the <python_call> ? <fallback_value> can be
used to provide a fallback value.
Boiling point of water: bp_water = flaky_simulation() ? 373.15 :K
Check out Oneil’s Python API for more details on how to use Oneil with Python.
Model imports
Models can import other models as submodels using submodel <file> as <alias>.
Parameters from the imported model are accessed as parameter.alias.
# satellite.on
submodel battery
submodel magnetometer as m
submodel radar as r
Satellite peak power: P_max = P_max.m + P_max.r :W
$ Battery usage: U_B = P_max / load_max.battery :%
# battery.on
Maximum load: load_max = 120 :W
# magnetometer.on
Magnetometer peak power: P_max = 20 :W
# radar.on
Radar peak power: P_max = 2 :W
A submodel correlates to a subsystem of the system being modeled. When you
need to read from a model but don’t want to treat it as a subsystem, use
reference <file> as <alias> instead.
# orbit.on
reference constants as c
Altitude of satellite: h = 500 :km
$ Radius of orbit: r = h + R_E.c :km
# constants.on
Earth radius: R_E = 6356752 :km
Designs
A design allows you to change or augment parameters of a model to represent
an alternative configuration. Designs are written in .one files and can be
applied to a model at the command line, to a model from a design using apply <design> to <alias>, or imported like a regular model submodel <design> as <alias>.
# mars.one
design planet
g = 3.72 :m/s^2
# mission.on
submodel mars as p
Spacecraft mass: m = 500 :kg
Surface weight: W = m * g.p :N
See Designs for the full reference.
Installation
This section describes how to install the Oneil CLI (Rust implementation) on Linux, Windows, and macOS. The recommended path for most users is to download a pre-built binary from GitHub Releases.
Option 1: Download a release from GitHub
Pre-built binaries are published on the Releases page for:
- Linux —
x86_64-unknown-linux-gnu(system,uv) - Windows —
x86_64-pc-windows-msvc(system,uv) - macOS —
aarch64-apple-darwin(Apple Silicon;homebrew,system,uv)
The CLI does not ship Python. Each archive is linked against a specific Python 3.12 layout. The VS Code / Cursor extension detects which layout you have and downloads that flavor. For a manual install, pick the matching archive:
- homebrew —
brew install python@3.12(macOS) - system — python.org 3.12 on macOS, distro
libpython3.12on Linux, or Python 3.12 onPATHon Windows - uv —
uv python install 3.12. The extension sets the library search path when it launches the CLI. For aPATHinstall, prefer Homebrew/system or build from source so the binary is linked to your uv prefix.
Pushing a version tag (for example v1.0.0) runs the Release workflow, which builds these archives and attaches them to the GitHub Release for that tag. In GitHub Actions, prefer careweather/oneil/actions/install-oneil (or model-test-report for full model-repo CI) — see Appendix C.
Linux / macOS
The release also attaches install-oneil.sh, which detects Homebrew / uv / system Python 3.12 and downloads that archive into ~/.local/bin:
curl -fsSL https://github.com/careweather/oneil/releases/latest/download/install-oneil.sh | bash
# or a specific tag:
curl -fsSL https://github.com/careweather/oneil/releases/download/v1.0.0/install-oneil.sh | bash
To pick the archive yourself:
-
Open the latest release.
-
Download the archive for your OS, architecture, and Python layout (for example
oneil-v1.0.0-x86_64-unknown-linux-gnu-system.tar.gzoroneil-v1.0.0-aarch64-apple-darwin-homebrew.tar.gz). -
Unpack and put the
oneilbinary on yourPATH:tar -xzf oneil-v*-x86_64-unknown-linux-gnu-system.tar.gz sudo mv oneil /usr/local/bin/ # or, without sudo: mkdir -p ~/.local/bin && mv oneil ~/.local/bin/ # ensure ~/.local/bin is in your PATH -
Confirm:
oneil --version
Windows
-
Open the latest release.
-
Download the Windows zip for your Python layout (for example
oneil-v1.0.0-x86_64-pc-windows-msvc-system.zip). -
Unzip and either move
oneil.exeinto a directory on yourPATH, or add the folder containingoneil.exeto yourPATH. -
Confirm in PowerShell or Command Prompt:
oneil --version
Option 2: Nix
If you use Nix with flakes enabled, you can run or install Oneil without a separate Rust or Python setup. The flake links against CPython 3.12 from nixpkgs and includes it at runtime.
To try Oneil without adding it to a system configuration:
nix run github:careweather/oneil -- --help
nix run github:careweather/oneil -- path/to/model.on
To install it, add the flake overlay and pkgs.oneil to your NixOS or home-manager configuration:
{
inputs.oneil.url = "github:careweather/oneil";
# nixpkgs.overlays = [ inputs.oneil.overlays.default ];
# environment.systemPackages = [ pkgs.oneil ]; # NixOS
# home.packages = [ pkgs.oneil ]; # home-manager
}
The overlay also provides the VS Code / Cursor extension as
pkgs.vscode-extensions.careweather.oneil. That package defaults
oneil.serverPath to the flake-built CLI, so the editor uses the same
CPython-linked binary as pkgs.oneil instead of downloading a GitHub
Release. Example with home-manager:
{
# nixpkgs.overlays = [ inputs.oneil.overlays.default ];
# programs.vscode.profiles.default.extensions = [
# pkgs.vscode-extensions.careweather.oneil
# ];
}
You can also build the extension with nix build github:careweather/oneil#oneil-vscode.
The first evaluation compiles from source. Contributors can use nix develop in the repository for the Rust toolchain, Python 3.12, and VS Code extension tools.
Prerequisites for building from source
The options below build Oneil yourself. You will need:
-
Rust: rustup — install and ensure
cargois on yourPATH. -
gcc
- Install on Fedora/RHEL:
sudo dnf install gcc - Install on Debian/Ubuntu:
sudo apt install build-essential
- Install on Fedora/RHEL:
-
Python 3.12 — the only CPython version Oneil supports. Needed at runtime when models
importPython modules, and when building from source (development headers). Install it with one of:- uv:
uv python install 3.12 - Homebrew:
brew install python@3.12 - Fedora/RHEL:
sudo dnf install python3.12-devel - Debian/Ubuntu:
sudo apt install python3.12-dev
Helper
.pyfiles canimport oneilbecause the CLI includes the Python library. - uv:
Option 3: Install from source using the install script
From the repository root, the install script builds the Rust CLI with default features (so models can import .py files and those files can import oneil).
git clone https://github.com/careweather/oneil.git
cd oneil
./install.sh
On Windows, use install.bat.
You need Python 3.12 (uv python install 3.12 or brew install python@3.12). The script prefers uv python find 3.12, then Homebrew python@3.12.
Option 4: Install from source with Cargo
Use this if you want the latest development version or need to customize the build.
-
Clone the repository:
git clone https://github.com/careweather/oneil.git cd oneil -
Build and install the
oneilbinary (requires Rust):cargo install --path src/oneilIt places
oneilin~/.cargo/bin(or%USERPROFILE%\.cargo\binon Windows); keep that directory on yourPATH.Building from source requires Python 3.12 (see Prerequisites).
-
Confirm:
oneil --version
Option 5: Run from the repository (development)
For day-to-day development without installing:
git clone https://github.com/careweather/oneil.git
cd oneil
cargo build -p oneil
./target/debug/oneil --version
# or run directly:
cargo run -p oneil -- path/to/model.on
Updating
- Release binary: download the newer archive from Releases and replace the previous
oneilbinary on yourPATH. - Nix: bump the
oneilflake input in your configuration and rebuild. - From source: pull the latest code (or check out the new tag), then re-run
./install.shorcargo install --path src/oneil.
Editor and tooling (optional)
-
VS Code / Cursor: Install the Oneil extension from the Marketplace for LSP and syntax highlighting, or install
pkgs.vscode-extensions.careweather.oneilfrom this flake (see Option 2: Nix). The Marketplace extension can download the Oneil CLI from GitHub Releases (Command Palette: “Oneil: Install or Update CLI”, or “Oneil: Select CLI Version…” to install a different published tag). It picks the Homebrew, system, or uv archive that matches the Python 3.12 on the machine. Setoneil.serverPathonly when you want to force a local build; that setting disables managed updates. The Nix package already setsoneil.serverPathto the flake-built CLI. -
Vim: See the Vim support section in the main README for syntax highlighting.
Uninstalling Oneil
If Oneil was installed as a release binary, delete the release binary.
If Oneil was installed with Nix, remove pkgs.oneil from your NixOS or home-manager configuration and rebuild.
If Oneil was installed from source, run cargo uninstall oneil.
If the Python library was installed with pip, run pip uninstall oneil in the same virtual environment.
Troubleshooting
-
oneil: command not found
Ensure the directory containing theoneilbinary is on yourPATH. -
Python-related build errors (from source) or
oneil --versionaborts
Install Python 3.12 to match the archive flavor you downloaded (brew install python@3.12, the python.org 3.12 installer, distropython3.12, oruv python install 3.12). See Prerequisites. The extension picks the flavor for you. -
Permission denied (Linux/macOS)
After moving the binary, runchmod +x /path/to/oneil(or the path you used). -
macOS: “cannot be opened because the developer cannot be verified”
Right-click the binary → Open, or remove the quarantine attribute:xattr -d com.apple.quarantine /path/to/oneil.
Parameters
Parameters are the main way to define values in an Oneil model. Parameters are variables with extra metadata for system modeling and review. They define a long name, limits, a math symbol for rendering and review, units, a derivation, and either a value assignment or an equation relating this parameter to others. This section covers the basics: defining parameters, running a model, and selecting what gets printed.
Hello world
A minimal Oneil model is a single parameter. Create a file hello.on with:
Hello world: hw = 1
Run it with:
oneil eval hello.on --params hw
The --params hw option prints the parameter hw. This option can also be shortened to -p hw. You should see something like:
hw = 1 # Hello world
oneil eval <model>.on is how you run an Oneil file. The output shows the model path, test summary, and each parameter’s identifier, value, and label (after #). In addition, both oneil e <model>.on and oneil <model>.on can be used as an equivalent to oneil eval <model>.on.
Required parts of a parameter
Each parameter declaration has three required pieces:
-
Name - A human-readable name (can include spaces). This can contain any character except the following:
(,),[,],{,},#,~,:,=,\n,*, and$. -
Identifier - The identifier used in expressions (e.g.
x). It must appear after the colon and before=. Other parameters and expressions refer to the parameter by this identifier. A name starts with a letter and may contain letters, digits, and underscores (_). -
Expression - The expression on the right-hand side of
=. It can be a number, a reference to another parameter, or a more complex expression (with optional unit; see Value Types and Units).
The syntax is:
Name: identifier = expression
Optionally insert a LaTeX render-name in braces after the colon so the Rendered View (and equation display) uses that symbol instead of deriving one from the identifier:
Name: {LaTeX} identifier = expression
For example,
Number of batteries: count = 10
Velocity: {\hat{v}} v = 0
Surface area: {A_{\mathrm{s}}} A = 4 * pi * R^2
Here, Number of batteries / Velocity / Surface area are labels, count / v / A are identifiers, and {\hat{v}} / {A_{\mathrm{s}}} are optional render-names. If you omit the braces, the viewer still picks a reasonable math symbol from the identifier (e.g. omega → $\omega$, A_hab → $A_{hab}$). See Notes for how those symbols appear with notes and {{…}} interpolation.
Comments
Comments in Oneil are prefixed by # and go until the end of the line.
# this is a comment
My param: p = 10
My other param: o = 5 # comments don't have to start at the beginning of a line
Running a model and viewing output
To evaluate a model file, use:
oneil eval <model>.on
For example:
# antenna.on
Antenna length: a_l = 5
oneil eval antenna.on
(No performance parameters found)
Note that there are no parameters printed out. The reason for this is discussed in Annotations.
For now, use --print all to print out all parameters.
oneil eval antenna.on --print all
a_l = 5 # Antenna length
Note
Throughout this guide, we will insert a comment at the top of each model indicating the name of the model.
# my_model.on ...This way, we can reference it when running
oneil eval.oneil eval my_model.on
Multiple parameters and references
You can define multiple parameters in one file. They can reference each other by name, and the order of declarations does not matter - Oneil resolves dependencies automatically.
For example,
# satellite.on
Body length: l_body = 25
Antenna length: l_a = 5
Satellite length: l_sat = l_body + l_a
oneil eval satellite.on --print all
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
Selecting parameters to print
By default, only parameters with certain annotations are printed. To print specific parameters by name, use --params (or -p):
oneil eval <model>.on --params param1,param2
# or
oneil eval <model>.on -p param1,param2
For example,
oneil eval satellite.on -p l_body,l_a
l_body = 25 # Body length
l_a = 5 # Antenna length
The order in the comma-separated list is the order they appear in the output. You can select one or more parameters; only those are printed.
Annotations
Parameters can be marked with optional annotations that control whether they are printed by default and how they are used:
| Annotation | Symbol | Meaning |
|---|---|---|
| Trace | * | Included when printing “trace” parameters (default). |
| Debug | ** | Same as trace, and with --debug / -D, extra debug info is printed for variables used to evaluate this parameter. |
| Performance | $ | Marked as a performance variable. |
Annotations appear before the label.
# satellite2.on
# No annotation
Mass: m = 10
# Trace annotation:
* Body length: l_body = 25
## Debug annotation:
** Antenna length: l_a = 5
# Performance annotation:
$ Satellite length: l_sat = l_body + l_a
oneil eval satellite2.on
l_sat = 30 # Satellite length
With the default print mode (perf), only the performance variables are displayed.
To display the other annotated variables, use the --print/-P argument with the trace argument.
oneil eval <model>.on --print trace
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
To display all variables, including non-annotated variables, use --print all.
oneil eval <model>.on --print all
m = 10 # Mass
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
Evaluating expressions
While it is usually best to include all equations in the model itself, there may
be some times when you need to do a quick evaluation of an expression. For that,
Oneil provides the --expr/-x argument. This argument prints out the result
of evaluating the provided expression. The expression can include parameters
from the model.
# orbit.on
Orbit radius: r = 7000
# determine the orbit circumference
oneil eval orbit.on --expr "2*pi*r"
2*pi*r = 43982
In addition, you can provide multiple expressions at the same time.
# determine the orbit circumference and diameter
oneil eval orbit.on --expr "2*pi*r" --expr "2*r"
# or, using the shorter argument name
oneil eval orbit.on -x "2*pi*r" -x "2*r"
pi*r^2 = 1.539e8
2*r = 14000
Value Types
Parameter values can include literals, equations, and imported functions. The main literal types are numbers, strings, and booleans.
Numbers
Numbers use familiar decimal notation: whole numbers, values with a decimal point, and scientific notation (e or E) when a value is very large or very small. inf can be used for infinity.
For example,
100th prime number: p_100 = 541
Golden ratio: phi = 1.618
Avogadro constant: N_A = 6.022e23
Infinity: inf_val = inf
Number Operators
Arithmetic (for ordinary scalar values):
^- exponentiation*- multiplication/- division%- modulo+- addition-- subtraction
Comparisons (produce booleans; can be chained, e.g. 1 < 2 < 3):
<- less than>- greater than<=- less than or equal>=- greater than or equal==- equal!=- not equal
Built-in pi and e
The identifiers pi and e are built-in numeric constants (π and Euler’s number). They can be used like any other value in expressions.
Strings
Strings behave like fixed strings, not like growable text in many other languages. Typical uses include modes or categories. For example, a battery’s array configuration could be either 'series' or 'parallel'. As another example, a remote sensing resolution mode might be 'polar', 'track', or 'footprint'.
A string is written in single quotes '...'. Double quotes are not used for strings.
String Operators
There is no concatenation or mutation. Strings can only be compared for equality:
==- equal!=- not equal
String Examples
# battery.on
Battery configuration: config = 'series'
oneil eval battery.on \
-x "config == 'series'" \
-x "config == 'array'"
config == 'series' = true
config == 'array' = false
Booleans
Boolean literals are the keywords true and false.
Boolean Operators
not- logical NOT (unary)and- logical ANDor- logical OR
Boolean Examples
Comparisons on numbers (and string equality) also yield booleans. Booleans are used in piecewise parameter conditions and in test declarations; those topics appear in later chapters.
Units
One of Oneil’s defining features is its unit-based type system. Oneil tracks units, disallows invalid operations between different physical properties, and automatically converts between differing units of the same physical property.
For example, Oneil will throw an error if you try to add a time and a distance or compare a mass and a temperature. But it will automatically convert a length in meters and a length in feet to a common base before adding them together.
This simplifies expressions to focus on relationships between physical properties while preventing unit conversion errors that might crash your spacecraft.
Units, dimensions, and magnitude
Before we get into how units work in Oneil, we’re going to take a quick detour to delve into what makes units compatible. Why can you add meters and kilometers, but not meters and kilograms? Why is a Joule equivalent to a Watt-second?
The answer is dimensions. A dimension can be defined as an aspect of something that can be measured. That definition is hard to understand on its own, though, so lets consider the dimension of time as an example.
A trip to the store (and more)
When measuring how long a car takes to get from your house to the store, it doesn’t matter whether you measure in seconds, minutes, or even millennia. They all are measurements of the dimension of time.
You can also measure how long it takes for you to get from the store to work, and you can measure that in any unit of time as well. Then, you could add the values together because they both measure the dimension of time.
However, it wouldn’t make sense to add the mass of your car to the measured travel time, since travel time is measured in the dimension of time, while car mass is measured in the dimension of mass.
Supported dimensions
Oneil supports the following dimensions, listed here with their associated base unit.
- mass:
kilogram - distance:
meter - time:
second - temperature:
Kelvin - current:
ampere - information:
bit - currency:
$(USD) - substance:
mole - luminous intensity:
candela
Base units convey a single dimension, like the unit kilometer with its
dimension distance. Derived units convey 0 to many dimensions, like the unit
degree which is dimensionless or the unit Joule with its dimensions of of
mass, distance^2, and time^-2. Dimensionless units are discussed in more
detail later in this chapter.
Each unit has 0 or more dimensions associated with it. The kilometer is defined
as having a dimension of distance, while a Joule would have the dimensions
of mass, distance^2, and time^-2.
There are also dimensionless units such %. These are discussed
later in this chapter.
If you haven’t quite wrapped your head around dimensions yet, don’t worry. You don’t need to fully understand it to use Oneil.
Magnitudes
So if kilometers and millimeters are the same dimensions, then what makes
them different? The difference is in the magnitudes.
A magnitude is the relative size of a unit compared to the base unit. Relative
to the base unit of meters, kilometers has a magnitude of 1000 since
1 km == 1000 m. Meanwhile, millimeters has a magnitude of 0.001 because
1 mm == 0.001 m.
Oneil tracks magnitudes and performs automatic conversions to handle units with
different magnitudes. So when Oneil sees 1 m + 1 km, it knows that it needs
to convert 1 km to 1000 m before adding. The result would therefore end up
being 1001 m.
This automatic conversion also applies to units such as feet and meters, as
well as more complex units like ft*lb/s^2 and Newtons, which could save your
climate orbiter from
a devastating crash.
Assigning units
Now that we’ve reviewed the motivation behind tracking units, let’s get into
the practical application. For parameters with a literal value, units can be
assigned with the :<unit> syntax:
# velocity.on
Distance: d = 100 :meters
Travel time: t = 20 :seconds
This will assign d to a value of 100 :meters and t to a value of
20 :seconds. These values are now measured numbers, or numbers with units.
Note
There are often multiple synonyms for a given unit. For example, the above model could also be written as
Distance: d = 100 :m Travel time: t = 20 :sTo see a list of all builtin units and their synonyms, run
oneil builtins unit. Also, if you would like to search for a given unit, runoneil builtins unit <unit>.
Annotating expected units
For calculated parameters, the :<unit> syntax declares the expected units of
a calculation, which Oneil checks.
For example, using d and t from the previous section, we can then define
velocity as
# velocity.on (continued)
$ Velocity: v = d/t :m/s
defining a parameter with a measured value with the units m/s.
Running the model with oneil eval velocity.on produces
v = 5 :m/s # Velocity
If we wanted to, we could just as easily define velocity in kilometers per hour.
$ Velocity: v = d/t :km/hr
# ^^^^^ `km/hr` instead of `m/s`
oneil eval velocity.on
v = 18 :km/hr # Velocity
Note that we did not have to do any conversions. Oneil handles that for us. However, if we try to use incorrect units, Oneil will produce an error.
$ Velocity: v = d/t :kg/hr
# ^^ `kg` instead of `km`
oneil eval velocity.on
error: calculated unit does not match expected unit
--> velocity.on:3:17
|
3 | $ Velocity: v = d/t :kg/hr
| ^--
= note: calculated unit is `meters/seconds` but expected unit is `kg/hr`
In addition, Oneil requires units on any parameters whose calculations are expected to produce a measured value. If we leave out the unit, we get an error.
Likewise, the calculation for a unitless parameter should not have a measured result. In that case, we do leave the units out (see dimensionless values).
$ Velocity: v = d/t
# ^ No unit
oneil eval velocity.on
error: parameter is missing a unit
--> velocity.on:3:17
|
3 | $ Velocity: v = d/t
| ^--
= note: parameter value has unit `meters/seconds`
= help: add a unit annotation `:meters/seconds` to the parameter
Composing units in a unit expression
A unit expression is built from one or more units separated by * or /.
Each unit can be raised to a numeric power with ^, such as s^2.
Unit expressions can also use the literal 1 as a dimensionless unit. This is
used in rates such as 1/s.
Warning
Multiplication and division operate left to right. So
J/kg*Kis treated as(J/kg)*Krather thanJ/(kg*K).To express
J/(kg*K), explicit parentheses are required.
Unit casting
Imagine that you have a test that takes time to start up before it runs. The full time of the test is 5 minutes and start-up time is 10 seconds. To calculate what the actual run time of the test is, you might write the following model.
# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - 10 :min
However, this will produce an error.
oneil eval testing.on
error: expected scalar with unit `min` but found scalar
--> testing.on:2:30
|
4 | Run time: t_r = t_f - 10 :min
| ^-
In other words, Oneil can’t determine whether 10 is supposed to be 10 seconds,
10 minutes, or 10 hours.
The first recommended solution is to create another parameter to hold this “magic number”. You can then define a unit on that parameter.
# testing.on
Full time: t_full = 5 :min
Startup time: t_start = 10 :s
$ Run time: t_run = t_full - t_start :min
oneil eval testing.on
t_run = 4.833 :min # Run time
However, there are some situations where you may just want to label a unitless number with a unit.
To do so, you can use unit casting. Unit casting takes the form of
(<expression> : <unit>). This allows a unitless value to be assigned a unit.
Using this, the model could be rewritten as
# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - (10:s) :min
oneil eval testing.on
t_run = 4.833 :min # Run time
Arithmetic and comparison operators
Arithmetic and comparison operator rules and behavior are defined by the
following table. The unit of a given value x is indicated by x_unit.
| Operation | Input Rules | Unit Output |
|---|---|---|
a + b, a - b, a % b | a_unit and b_unit must have the same dimensions | a_unit |
a * b | None | a_unit * b_unit |
a / b | None | a_unit / b_unit |
a ^ b | b cannot have any dimensions | a_unit ^ b |
comparison (<, >, <=, >=, ==, !=) | a_unit and b_unit must have the same dimensions | N/A (produces true or false) |
Examples
Note
empty.onis just an empty model, since we don’t reference any model parameters.
# addition, subtraction, modulo
oneil eval empty.on \
-x "(1000:m) + (1:km)" \
-x "(1:km) + (1000:m)" \
-x "(5:min) - (30:s)" \
-x "(80:s) % (1:min)"
(1000:m) + (1:km) = 2e3 :m
(1:km) + (1000:m) = 2 :km
(5:min) - (30:s) = 4.5 :min
(80:s) % (1:min) = 20 :s
# multiplication, division, exponentiation
oneil eval empty.on \
-x "(1:m) * (1:s)" \
-x "(1:m) * (1:m)" \
-x "(1:m) * 1" \
-x "(1:m) / (1:s)" \
-x "(1:m) / 1" \
-x "(1:m)^2"
(1:m) * (1:s) = 1 :m*s
(1:m) * (1:m) = 1 :m*m
(1:m) * 1 = 1 :m
(1:m) / (1:s) = 1 :m/s
(1:m) / 1 = 1 :m
(1:m)^2 = 1 :m^2
# comparison
oneil eval empty.on \
-x "(1:kg) < (2000:g)" \
-x "(1:kg) > (1:g)" \
-x "(1:kg) <= (1000:g)" \
-x "(1:kg) >= (900:g)" \
-x "(1:kg) == (1000:g)" \
-x "(1:kg) != (1:g)"
(1:kg) < (2000:g) = true
(1:kg) > (1:g) = true
(1:kg) <= (1000:g) = true
(1:kg) >= (900:g) = true
(1:kg) == (1000:g) = true
(1:kg) != (1:g) = true
strip
In the case that you would like to treat a measured value as unitless, Oneil
provides the strip function. The strip function removes any units from a
value.
# adc.on
ADC bit resolution: S_adc = 10 :b
$ ADC step count: n_adc = 2^(strip(S_adc)-1)
oneil eval adc.on
n_adc = 512 # ADC step count
The places where this should be used are rare and should be treated cautiously
since strip effectively disables unit checking.
In addition, it is important to realize that strip strips the unit that is
currently associated with a value.
# length.on
Length in meters: l_m = 1000 :m
Length in kilometers: l_km = 1 :km
oneil eval length.on \
-x "strip(l_m)" \
-x "strip(l_km)"
strip(l_m) = 1e3
strip(l_km) = 1
For this reason, when using strip, it is recommended that you first cast
the value to the unit that you expect it to be.
oneil eval length.on \
-x "strip((l_m :m))" \
-x "strip((l_km :m))"
strip((l_m :m)) = 1e3
strip((l_km :m)) = 1e3
Non-linear units
On top of linear units, Oneil supports decibel (dB) units. You form a
decibel unit by prefixing dB directly to a built-in unit name, for example
dBmW (decibels relative to one milliwatt) or dBV. The bare name dB
(with no following unit) is also valid; it behaves as a dimensionless logarithmic
unit.
Support for other non-linear units is on the roadmap.
When any unit is specified with prefix dB, Oneil internally converts the
parameter to the corresponding linear value, performs all calculations in linear
terms, and reconverts the value to dB for display. This means that equations
that contain parameters with dB units should use linear math. For example,
when calculating the signal to noise ratio by hand, you might subtract the noise
(dB) from the signal (dB), but in Oneil, you divide the signal by the noise:
# power.on
Noise power: P_n = -100 :dBmW
Signal power: P_s = -90 :dBmW
$ Signal-to-noise ratio: S_N = P_s/P_n
oneil eval power.on
S_N = 10 # Signal-to-noise ratio
Dimensionless units
There are some units that don’t have any dimensions, such as % or ppm (parts
per million). These units are referred to as dimensionless units, and values
with dimensionless units are referred to as dimensionless values.
Unitless equivalence
Dimensionless values can be treated as if they have no unit. The following
demonstrates this with the % unit.
Note
empty.onis just an empty model, since we don’t reference any model parameters.
# `100%` is treated as equal to `1`
oneil eval empty.on \
-x "(100:%) == 1" \
(100:%) == 1 = true
# the `1` is equal to `100%`, not `1%`
oneil eval empty.on \
-x "(100:%) + 1"
(100:%) + 1 = 200 :%
Angular Units
The lack of distinction between dimensionless values and unitless values is
especially important when it comes to units involving radians. The
International System of Units treats radians as dimensionless, and Oneil has
opted to follow this convention. Therefore, all angular units (such as
radians, degrees, and revolutions) are specified in radians. Therefore,
when adding a unitless number to an angular value, the unitless number is
treated as if it is specified in radians.
oneil eval empty.on \
-x "(1:rad) == 1" \
-x "(360:deg) == 2*pi" \
-x "(1:rad) + 1" \
-x "(360:deg) + 2*pi"
(1:rad) == 1 = true
(360:deg) == 2*pi = true
(1:rad) + 1 = 2
(360:deg) + 2*pi = 720 :deg
Hz and rad/s
There is one place where Oneil’s automatic conversions might cause confusion.
That is with the Hz unit. In order to solve the problem described
in this article
and make Hz compatible with rad/s, Oneil defines Hz as
1 Hz == 1 cycle/s == 2*pi rad/s.
Note that both cycles and radians are both dimensionless values, but
1 cycle == 2*pi radians.
# freq.on
Frequency: f = 1 :Hz
oneil eval freq.on \
-x "f" \
-x "(f :cycle/s)" \
-x "(f :rad/s)" \
f = 1 :Hz
(f :cycle/s) = 1 :cycle/s
(f :rad/s) = 6.283 :rad/s
By default, Oneil treats dimensionless values as if they are in radians.
Because of this, anytime you would like dimensionless values to be in cycles,
you need to manually convert from radians to cycles by dividing by 2*pi.
# freq2.on
Frequency: f = 5 :GHz
Speed of light: c = 299792458 :m/s
$ Wavelength: lambda = c/(f/2*pi) :cm
# ^^^^^ Need to divide by 2*pi to convert radians to cycles
oneil eval freq2.on
lambda = 0.6075 :cm # Wavelength
Tests
Alongside parameters, Oneil provides tests, which allows users to verify that certain properties, requirements, and expectations hold.
The syntax for tests is test: <test-expression>.
test: 1 + 1 == 2
Component A Length: L_A = 5 :cm
Component B Length: L_B = 3 :cm
Max Length: L_max = 10 :cm
test: L_A + L_B <= L_max
A test expression can be any expression that produces a boolean (true or
false). For more information, see Booleans
and Number operations.
Examples
The point of a test is to encode a requirement you care about: margins, safety limits, or physical feasibility.
Thrust versus gravity
For a vehicle to accelerate upward, thrust must exceed weight.
Dry mass: m_dry = 420 :kg
Propellant mass: m_prop = 180 :kg
Liftoff mass: m = m_dry + m_prop :kg
Sea-level gravity: g = 9.81 :m/s^2
Liftoff thrust: F_thrust = 7500 :N
test: F_thrust > m * g
Stress and material limit
Keep computed stress below the allowable value derived from yield strength and a safety factor:
Yield strength: sigma_y = 250 :MPa
Safety factor: SF = 2
Allowable stress: sigma_allow = sigma_y / SF :MPa
Working stress: sigma_work = 95 :MPa
test: sigma_work < sigma_allow
Operating temperature
Confirm a junction stays inside the part’s rated range. Temperature uses Kelvin as the underlying dimension:
Ambient: T_amb = 260 :K
Self-heating: delta_T = 40 :K
Junction temperature: T_j = T_amb + delta_T :K
Rated maximum junction: T_max = 400 :K
test: T_j <= T_max
Power budget
Check that available electrical power covers peak demand with headroom:
Supply capability: P_supply = 48 :W
Peak load: P_peak = 35 :W
Minimum design margin: P_margin_min = 5 :W
test: P_supply >= P_peak + P_margin_min
String modes and requirements
Tests can combine numeric checks with string equality if the model uses a parameter to indicate the mode:
Array configuration: config = 'series'
Cell count: n_cells = 12
Cells required for target voltage: n_req = 12
test: config == 'series' and n_cells == n_req
To run these checks automatically in a model repository’s CI, see Appendix C: Continuous Integration.
Intervals
In some cases, you may want to calculate using a range of values rather than one single value. This allows you to represent uncertainty. For example, maybe you don’t know what the wind speed will be exactly, but you can estimate that it will be between 5 and 10 kilometers per hour.
To enable this, Oneil provides an interval operator in the form of
<expr> | <expr>.
Note
You may also see references to this as the minmax operator, since it produces the min and the max values.
The interval operator takes two expressions and produces a value representing
the range from the minimum of the expressions to the maximum expressions. These
values can then be retrieved using the min and max functions.
# temperature.on
$ Ambient temperature: t_amb = 300 | 400 :K
$ Ambient temperature range: t_amb_range = max(t_amb) - min(t_amb) :K
oneil eval temperature.on
t_amb = 300 | 400 :K # Ambient temperature
t_amb_range = 100 :K # Ambient temperature range
Arithmetic Operators
The same operators that apply to scalar values apply to interval values: +,
-, *, /, %, and ^.
Because an interval is a range of possible values, not a single number, results can differ from the naive idea of “min with min, max with max.” For example, with subtraction, that naive rule would be wrong:
X: x = 10 | 15
Y: y = 0 | 5
Z: z = x - y
# = (10 | 15) - (0 | 5)
# = 10 - 0 | 15 - 5
# = 10 | 10 # incorrect
Oneil implements subtraction so the range is arithmetically correct:
min(i1) - max(i2) | max(i1) - min(i2).
X: x = 10 | 15
Y: y = 0 | 5
Z: z = x - y
# = (10 | 15) - (0 | 5)
# = 10 - 5 | 15 - 0
# = 5 | 15
For more detail on interval operators, see the interval arithmetic paper review or the implementation in the codebase.
Escaping and relationships
Oneil’s interval arithmetic aims to satisfy the inclusion property: if every interval in an expression is replaced by some scalar inside that interval and the expression is evaluated as scalars, the scalar result lies inside the interval you get by evaluating the expression on intervals.
Bounds can still be wider than necessary. For example, you would expect a - a to be 0 for any a. If a is 0 | 1, interval subtraction yields -1 | 1. That interval still contains the true result 0, but it is looser than
0 | 0. This know as the
dependency problem.
When you need tighter results (for example in geometry, where identities like
a - a = 0 matter), you can leave “pure” interval arithmetic by using
min(i) and max(i) to work on scalars, then build a new interval with |.
For instance, instead of a - a, you can use
min(a) - min(a) | max(a) - max(a).
For common cases, Oneil provides -- and //:
| Operator | Equivalent to |
|---|---|
a -- b | min(a) - min(b) | max(a) - max(b) |
a // b | min(a) / min(b) | max(a) / max(b) |
Comparison Operators
Intervals can be compared with ==, !=, <, <=, >, and >=. The rules
are defined in terms of min and max:
| Operator | Equivalent to | Description |
|---|---|---|
a == b | min(a) == min(b) and max(a) == max(b) | The min and the max are the same |
a != b | min(a) != min(b) or max(a) != max(b) | The min or the max is not the same |
a < b | max(a) < min(b) | The max value of a is less than the min value of b |
a <= b | max(a) <= min(b) | The max value of a is less than or equal to the min value of b |
a > b | min(a) > max(b) | The min value of a is greater than the max value of b |
a >= b | min(a) >= max(b) | The min value of a is greater than or equal to the max value of b |
Notes
Oneil renders parameter equations for review directly from code. This makes it easier to review code with complex equations.
If you’ve ever written a scientific paper, you know that there is often a lot of typeset math and narrative involved in deriving an equation. Showing your work like this helps you and others remember or review the reasons a parameter is expressed the way it is. To help you do this, Oneil supports notes — documentation attached to models, parameters, sections, and tests. Unlike # comments (which Oneil ignores), notes travel with the model and are shown in the Rendered View in the VS Code / Cursor extension.
Note bodies are Markdown (GitHub Flavored Markdown), with LaTeX math, parameter interpolation, and citations. Open a .on / .one file and run Oneil: Open Rendered View to see them formatted.
Parameter Notes
| Attachment | Placement |
|---|---|
| Model | At the top of the file (before parameters) |
| Parameter | Immediately after the parameter declaration |
| Section | Immediately after a section Label line |
| Test | Immediately after a test: … line |
Delimiters
Single-line notes start with ~:
Rotation rate: omega = 1 :deg/min
Cylinder radius: r = d/2 :km
~ The distance from the center of the cylinder to the inner rim.
You can use three tildes to start and end a multi-line note:
Artificial gravity: g_a = r*omega^2 :m/s^2
~~~
The position of a point on the rim of a rotating cylinder is:
$\vec{r}(t) = r\cos(\omega t)\,\hat{i} + r\sin(\omega t)\,\hat{j}$
Taking the first derivative gives the velocity:
$\vec{v}(t) = \frac{d\vec{r}}{dt} = -r\omega\sin(\omega t)\,\hat{i} + r\omega\cos(\omega t)\,\hat{j}$
Taking the second derivative gives the acceleration:
$\vec{a}(t) = \frac{d\vec{v}}{dt} = -r\omega^2\cos(\omega t)\,\hat{i} - r\omega^2\sin(\omega t)\,\hat{j} = -\omega^2\vec{r}(t)$
The acceleration points radially inward (toward the center), and its magnitude is:
$|\vec{a}| = r\omega^2$
This centripetal acceleration acts as artificial gravity for inhabitants
standing on the inner rim of the cylinder, so $g_a = r\omega^2$.
~~~
Sections and Section Notes
The section keyword will produce a header when rendered. Sections can be given their own notes:
Earth gravity: g_E = 9.81 :m/s^2
section Tests
~ The following tests ensure that the artificial gravity of the station won't exceed a \href{https://www.reddit.com/r/scifiwriting/comments/szwvep/what_is_the_highest_gravity_that_humans_could/}{livable range for human occupants}.
test : g_a < 1.1*g_E
Notes can also attach to the model (at the top of the file) and to individual test: lines.
Parameter LaTeX / rendered names
In the Rendered View, each parameter is shown with a math symbol. You can set that symbol explicitly with braces after the colon (before the identifier):
Velocity: {\hat{v}} v = 0 :m/s
Surface area: {A_{\mathrm{s}}} A = 4 * pi * R^2 :m^2
If you omit {…}, the viewer derives a symbol from the identifier (omega → $\omega$, A_hab → $A_{hab}$, and so on).
Markdown, math, and links
Besides inline $…$ math (as in the examples above), the Rendered View also supports display math ($$…$$), \begin{equation}…\end{equation}, and ordinary Markdown: headings, lists, tables, code, images, and links.
Markdown links work as [text](url).
Images use  (model-file directory first, then the workspace root). A leading / is workspace-root only. PDF paths in image links are recognized by the extension.
Parameter interpolation
Insert a live parameter into a note with {{identifier:mode}}:
| Placeholder | Meaning |
|---|---|
{{name:value}} | The parameter’s evaluated value (and unit) |
{{name:equation}} | The parameter’s defining expression |
Habitable area: A_hab = 3/6 * A_tot :km^2
~ Land stripes only: {{A_hab:equation}} = {{A_hab:value}}.
Placeholders also work inside math mode. See examples/oneil_cylinder.on for more.
Citations and bibliography
Citation syntax
Citations follow a Pandoc-style convention. Keys must match an entry in a BibTeX file (see below).
Bracketed (parenthetical)
| Syntax | Renders as |
|---|---|
[@key] | (Author, year) |
[@k1; @k2] | Multiple citations |
[+@key] | All authors listed |
[-@key] | (year) only |
[!@key] | Author name without parentheses |
[@key, p. 42] / pp. 12-15 | Open / jump to that page of the PDF |
~ Island Three is described in *The High Frontier* [@ONeill1977].
~ Crew health limits are in [@NASA-STD-3001, p. 12].
Textual (narrative)
| Syntax | Renders as |
|---|---|
@key | Author (year) |
+@key / -@key / !@key | Full authors / year / author only |
~ @ONeill1977 described Island Three in *The High Frontier*.
In the Rendered View, citations are clickable. The bibliography panel lists entries cited in the focused note.
Where to put the .bib file
Prefer a file named references.bib. Search order matches note images: the model file’s directory first, then the workspace. Most preferred first:
references.bibin the same directory as the open.on/.onefile- Other
*.bibfiles in that directory references.bibat each workspace folder root- Remaining
*.bibfiles anywhere in the workspace (shallower paths first;node_modulesskipped)
All readable matches are concatenated, so you can keep a shared workspace bibliography plus a model-local one.
A worked example lives in the repo at examples/references.bib, used by examples/oneil_cylinder.on.
Citing a PDF
When you click on [@key] (or [@key, p. N]) in the Rendered View the extension can open the PDF on the right page. To make sure that the extension can find the PDF ensure that you setup references.bib as follows:
1. Put a direct PDF url on the BibTeX entry
Prefer url set to a file that actually serves a PDF. That is what Download & Cache needs, so later clicks open the cached copy instead of a browser.
@techreport{NASA-STD-3001,
author = {{NASA}},
title = {NASA Space Flight Human-System Standard, Volume 1: Crew Health},
year = {2014},
url = {https://standards.nasa.gov/.../nasa-std-3001-....pdf},
}
A doi alone (bare identifier, e.g. 10.1088/1681-7575/ac0240) opens https://doi.org/… — a publisher page, not a PDF — so each click goes to the browser. Use doi as extra metadata if you like; do not rely on it for caching.
2. Cite it in a note
~ See the crew health limits [@NASA-STD-3001, p. 12].
The page locator (p. 12, pp. 12-15, or a bare , 12) is what jumps the PDF viewer. For a default page whenever the cite has no locator, set pdfpage = {12} on the BibTeX entry.
3. Let the extension cache the PDF
- Open the model in Oneil: Open Rendered View.
- Click the citation.
- When prompted, choose Download & Cache. The file is stored under the PDF cache directory (default
~/.local/oneil/resources/, override withoneil.pdf.cacheDir). - When prompted, choose Update references.bib. The extension writes a portable
filefield, e.g.:
file = {:nasa-space-flight-human-system-standard-volume-1_488d61c2.pdf:PDF},
That bare filename is resolved back through the cache directory, so teammates on other machines can re-download (or share the cache) without absolute paths.
Turn on Oneil: PDF Auto Download (oneil.pdf.autoDownload) if you want step 3 without the prompt. Oneil: Toggle PDF Offline Mode only opens already-cached files.
4. Optional: point file at a project PDF yourself
If the PDF already lives in the repo (or anywhere on disk), set file manually instead of using the cache:
file = {:./papers/nasa-std-3001.pdf:PDF},
% or an absolute path / JabRef-style Description:path:PDF
Resolution order when you click a cite:
- BibTeX
file(absolute,~/…,./…relative to the model file then the workspace root, or bare name in the PDF cache) - Cache entry already downloaded for that URL
- Download from
urlwhen the response is a PDF (unless offline), then optionally updatereferences.bib - Open
url, orhttps://doi.org/<doi>if there is nourl, in the browser
Useful BibTeX fields
| Field | Role |
|---|---|
author, title, year | Citation labels and bibliography list |
url | Direct PDF link (recommended). Used to download into the cache |
doi | Bare identifier, e.g. 10.1088/1681-7575/ac0240. Opens the publisher page; does not cache a PDF |
file | Local or cache path (:filename.pdf:PDF, ./relative.pdf, or absolute) |
pdfpage | Default page when the cite has no p. / pp. locator |
Importing models
Models rarely stand alone. When your system is made of parts — subsystems, environments, shared constants — you’ll want to pull those in from other files. Oneil gives you two ways to do that: reference and submodel.
References
A reference makes another model’s parameters available under an alias. Use it for shared parameters and models — physical constants, material properties, standard environments — that belong to the world your system lives in, not to the system itself.
For example with the following constants:
# constants.on
Speed of light: c = 299792458 :m/s
Planck constant: h = 6.626e-34 :J*s
Boltzmann constant: k_B = 1.38e-23 :J/K
Gravitational constant: G = 6.674e-11 :N*m^2/kg^2
We can model both a photon’s energy
# photon.on
reference constants as c
Photon frequency: f = 5.09e14 :Hz
Photon energy: E = h.c * f :J
As well as an radio link
# link_budget.on
reference constants as c
Distance: d = 384400 :km
Signal frequency: f = 2.4e9 :Hz
Path loss: L_fs = (4 * pi * d * f / c.c)^2
Creating a short alias using as alias_name is optional. Without the alias you access parameters using the model’s name:
# photon_ref_constants.on
reference constants # no alias
Photon frequency: f = 5.09e14 :Hz
Photon energy: E = h.constants * f :J
Submodels
A submodel adds a model as part of the current model and gives access to the other model’s parameters.
Unlike a reference, each submodel statement creates an independent model — if you import the same model twice
under different aliases each one has its own parameters, which you can change independently by applying a design file to that instance (see Designs).
Planets are a natural fit — a mission might visit multiple planets, and each needs its own independent parameters.
# planet.on
Surface gravity: g = 9.81 :m/s^2
Radius: R = 6371 :km
Mass: M = 5.97e24 :kg
The planet model defaults to Earth’s values on earth, but submodel mars imports the mars design file which applies a mars design to the planet instance (m): its bindings override g, R, and M, and add Mars-specific parameters. The two instances stay independent.
# mission_earth_mars.on
submodel planet as earth
submodel mars as m
Spacecraft mass: m = 500 :kg
Weight on Earth: W_e = m * g.earth :N
Weight on Mars: W_m = m * g.m :N
Like reference imports, the name after as is an alias. If no alias is given, the default alias is the model name:
# submodel_planet_default_alias.on
submodel planet
Surface gravity seen: g_l = g.planet :m/s^2
It is an error for two submodels to have the same alias.
Accessing submodel parameters
To access a parameter inside a reference or submodel, write parameter_id.alias
— the parameter comes first, the model’s alias second.
# orbital_speed.on
reference constants as c
submodel planet as p
Orbital speed: v = sqrt(G.c * M.p / R.p) :m/s
Note
This is the reverse of the
object.propertyconvention in most programming languages, and it is intentional. In engineering equations parameters are primary, whereas subscripts often qualify which subsystem or model the parameter is part of.
Referring to a nested submodel
A submodel is also exported as part of the current model’s structure. A parent can reach nested parameters by importing a reference to a nested submodel using a local alias.
For example with solar system defined as follows:
# solar_system.on
submodel planet as earth
submodel mars
Star mass: M_s = 1.989e30 :kg
Earth orbital period: T_e = 365.25 :days
Earth surface gravity: g_s = g.earth :m/s^2
The syntax [alias] or [alias as local_alias] exposes nested submodels at the
current scope. Names inside the brackets are the aliases declared inside the
imported model - not the source file names. Here earth is the alias from
submodel planet as earth, and mars is the default alias for submodel mars.
# mission_sol.on
# Import the solar system and declare `e` as a local alias for `earth`,
# so it can be used directly at the mission level.
submodel solar_system as sol [earth as e]
Probe mass: m_p = 800 :kg
# Access the parameters of e here
Landing weight: W = m_p * g.e :N
# Access solar-system parameters as normal
Star mass: M_s = M_s.sol :kg
Earth orbit: T = T_e.sol :days
You can pull in multiple aliases by separating them with commas:
# mission_two_targets.on
submodel solar_system as sol [
earth as e,
mars as t # landing target
]
Probe mass: m_p = 800 :kg
Weight on Earth: W_e = m_p * g.e :N
Landing weight on target: W_t = m_p * g.t :N
Designs explains design files (design <target> in a .one file): you apply a design to specific reference / submodel instances. A design overrides existing parameters (same name as on the target model) and can add new ones.
Deeper nesting
When the submodel you need sits more than one level down, use a dotted path of aliases inside the import list.
For example, take a radar application on a satellite with an orbit:
# orbit.on
Orbital speed: v = 3 :km/s
# satellite.on
submodel orbit as o
Satellite mass: m = 50 :kg
# radar.on
submodel satellite as sc
Transmit power: P_t = 100 :W
A model with a radar submodel can import orbit in one line by walking the
alias path sc.o (satellite alias, then orbit alias):
# mission_radar.on
submodel radar as r [sc.o as o]
Noise term uses orbit speed: n = v.o :km/s
Radar power: P = P_t.r :W
Without as o, the local alias defaults to the last segment of the path
(o in sc.o), so submodel radar as r [sc.o] is equivalent here.
Importing only creates a local name for an existing nested instance; it does
not import a new copy. After importing, parameters are still accessed as
parameter.local_alias (for example v.o), the same as any other submodel.
Designs
A design file (extension .one starting with design <target>) is how you apply changes without editing the target model. Bindings in the file either override parameters that already exist on the target (same identifier) or add new parameters. Designs can also be applied to specific reference / submodel instances, and they can override parameters on those models directly using <param>.<alias> = <value> syntax.
Use design files to explore “what if” scenarios: alternative materials, different components, different environments, different mission parameters, and so on.
Design files
Start your design file with a design <target> declaring the model you’re refining. For the parameters themselves, you can use shorthand — just identifier = expression with an optional :unit, skipping the label that model files require.
With the following model of a planet:
# planet.on
Surface gravity: g = 9.81 :m/s^2
Radius: R = 6371 :km
Mass: M = 5.97e24 :kg
You can define Mars by applying a design file that overrides planet’s base parameters and adds extra ones:
# mars.one
design planet
g = 3.72 :m/s^2
R = 3390 :km
M = 6.417e23 :kg
Number of active rovers: rovers = 2
Mars solar day: t_sol = 24.6 :hr
Surface area: A = 4 * pi * R^2 :km^2
Note
- If a name matches a parameter on the target model (
g,R,M), your value or equation overrides it.- If the name does not match any existing parameters (rovers, t_sol, A), the design adds a new parameter to the model.
- Equations may refer to parameters from the design file or target model file.
- Equations and parameters are evaluated after all models in the system are built and every design in play has been applied.
Running a design file directly
Because the design file declares its target model, you can run it directly:
oneil eval mars.one -P all
This command evaluates planet.on with the mars.one design applied and shows all parameters, including the new ones we’ve added:
g = 3.72 : m/s^2 # Surface gravity
R = 3390 : km # Radius
M = 6.417e23: kg # Mass
rovers = 2 # Number of active rovers
t_sol = 24.6 : hr # Mars solar day
A = 1.444e8 : km^2 # Surface area
Alternatively you can pass the --design flag after the model file.
oneil eval planet.on --design mars.one
Applying a design from within a model file
In model files you can apply a design to a submodel by naming the design file in a submodel clause. Oneil reads the design <target> line in that file, loads the underlying model, and applies the design.
# mission.on
submodel mars as m
Spacecraft mass: m = 500 :kg
$ Surface weight: W = m * g.m :N
oneil eval mission.on
W = 1860 : N # Surface weight
If the design file does not exist, or its design declaration names a model that cannot be loaded, you will get an error.
You can also apply a design to an instance you already imported using apply <design_file> to <alias>
submodel planet as m
apply mars_design to m
Inside a design file, use apply <design> to <alias> for submodels and references that are already declared on the target model; a design file cannot declare new submodel lines of its own.
Design isolation and scoped parameter overrides
Whether you use reference or submodel determines how broadly an applied design takes effect.
Dotted param.alias = value syntax in a design file is how you write scoped overrides that change parameters within the imported models.
reference creates a shared instance: every model that imports the same file under any alias sees the same parameters.
A scoped override of param.ref changes the parameter everywhere the model is imported as a reference.
submodel creates an independent instance: two submodel imports of the same file are completely isolated.
A scoped override on one alias never changes the other.
In the following example, mission_budget.on uses both kinds of imports.
constants is a reference — mission delta-v dv is shared.
rover_a and rover_b are independent submodels — a design file can override parameters on each rover separately.
The propellant model is a single-burn maneuver obeying the rocket equation, Δv = v_e ln(m_wet / m_dry), rearranged to m_req = m_dry (e^(Δv / v_e) − 1) as in the model below.
v_e is effective exhaust velocity, packaging engine efficiency (specific impulse).
Dry mass here means mass after the maneuver — stage structure plus payload (m_bus plus rover masses). Wet mass is that dry mass plus the propellant needed for the burn.
# constants.on
Delta-v budget: dv = 2850 :m/s
# rover.on
Rover mass: m = 1000 :kg
# mission_budget.on
reference constants as c
submodel rover as rover_a
submodel rover as rover_b
Vehicle dry mass — structure and engines, excluding propellant and rovers: m_bus = 4000 :kg
Rover payload mass: m_pl = m.rover_a + m.rover_b :kg
$ Dry mass after maneuver: m_dry = m_bus + m_pl :kg
Effective exhaust velocity: v_e = 3000 :m/s
Loaded propellant available: m_p0 = 15000 :kg
Tsiolkovsky propellant required: m_req = m_dry * (e ^ (dv.c / v_e) - 1) :kg
$ Wet mass at ignition: m_wet = m_dry + m_req :kg
$ Propellant margin(-1e30, 1e30): m_mg = m_p0 - m_req :kg
$ Rover A mass: m_a = m.rover_a :kg
$ Rover B mass: m_b = m.rover_b :kg
test: m_req < m_p0
Rover mass impacts m_dry through m_pl, so heavier rovers increase required propellant.
The basic mission profile passes with extra propellant margin.
oneil mission_budget.on
────────────────────────────────────────────────────────────────────────────────
Model: mission_budget.on
Tests: 1/1 (PASS)
────────────────────────────────────────────────────────────────────────────────
m_dry = 6e3 : kg # Dry mass after maneuver
m_wet = 15514 : kg # Wet mass at ignition
m_mg = 5486 : kg # Propellant margin
m_a = 1e3 : kg # Rover A mass
m_b = 1e3 : kg # Rover B mass
The following design overrides the shared dv.c binding in constants to model a costlier Δv requirement.
# high_dv.one
design mission_budget
dv.c = 4000 :m/s
oneil high_dv.one
────────────────────────────────────────────────────────────────────────────────
Model: mission_budget.on
Tests: 0/1 (FAIL)
────────────────────────────────────────────────────────────────────────────────
FAILING TESTS
mission_budget.on
test: m_req < m_p0
- m_req = 16762 : kg
- m_p0 = 1.5e4 : kg
────────────────────────────────────────────────────────────────────────────────
m_dry = 6e3 : kg # Dry mass after maneuver
m_wet = 22762 : kg # Wet mass at ignition
m_mg = -1762 : kg # Propellant margin
m_a = 1e3 : kg # Rover A mass
m_b = 1e3 : kg # Rover B mass
You can override rover mass on one submodel only. The mission design below raises m.rover_a and causes the mission to break the propellant budget; m.rover_b stays at its model default.
# heavy_rover.one
~ Heavy-duty rover A: too massive for the propellant budget.
design mission_budget
m.rover_a = 8000 :kg
oneil heavy_rover.one
────────────────────────────────────────────────────────────────────────────────
Model: mission_budget.on
Tests: 0/1 (FAIL)
────────────────────────────────────────────────────────────────────────────────
FAILING TESTS
mission_budget.on
test: m_req < m_p0
- m_req = 20614 : kg
- m_p0 = 1.5e4 : kg
────────────────────────────────────────────────────────────────────────────────
m_dry = 1.3e4 : kg # Dry mass after maneuver
m_wet = 33614 : kg # Wet mass at ignition
m_mg = -5614 : kg # Propellant margin
m_a = 8e3 : kg # Rover A mass
m_b = 1e3 : kg # Rover B mass
Note
- If a scoped override assigns an equation, it is evaluated in the design file’s scope, so you can refer to other bindings in that same file.
- A value with incompatible units, or an override of a name that does not exist on the instance you targeted with a scoped override, is an error.
- To change many parameters on one submodel — or to add parameters to it — prefer a dedicated design file whose
designtarget is that submodel’s base model.- Rule of thumb: import shared environmental data (constants, tables, etc) as
reference. Import components and system elements assubmodel.
Applying designs to submodel aliases
When you declare a local alias for a nested submodel (described in Importing models), the local alias and the one inside the intermediate model are two names for the same model. A design change applied through either name takes effect on both.
Assuming the following galaxy model:
# galaxy.on
submodel solar_system as sol [earth as e]
Probe mass: m_p = 800 :kg
$ Landing weight: W = m_p * g.e :N
$ Sol gravity reading: g_s = g_s.sol :m/s^2
We can apply the mars design to the e instance so Earth’s parameters are overridden by the Mars design file:
# earth_as_mars.one
design galaxy
apply mars to e
oneil earth_as_mars.one
W = 2976 : N # Landing weight
g_s = 3.72 : m/s^2 # Sol gravity reading
Because galaxy.e and sol.e are both aliases to the same model, both parameters pick up the change to gravity — W directly through g.e, and g_s through sol.
Importing Python Functions
Oneil can call functions defined in ordinary .py files. That uses the Python 3.12 already installed on the machine (the CLI does not ship Python). Helper modules may import oneil for Interval, MeasuredNumber, units, and builtins — the CLI injects that module into the installed interpreter (see Appendix A).
For calculations or simulations that need Python, after you implement the function in python simply import the Python file into Oneil:
import <path to functions file>
The path is sibling-relative to the .on file (no .py extension), using the same slash form as submodel / reference:
import functions
import ../functions
import ../testing/helpers
import simulations/compass/simmodel/simgeom
import functions looks for functions.py next to the model. A model in a subfolder that needs the parent file writes import ../functions. While that file loads, and again when its functions run, Oneil puts its directory on Python’s sys.path so the script can import other modules from its own folder. Installed packages and virtualenv modules stay on the existing sys.path and in sys.modules.
The imported file should define functions matching the names used in parameters:
import numpy as np
def temperature(transit_mode):
...
In the Oneil file, give the python function on the right hand of the equation, including other parameters as inputs:
Temperature: T = temperature(D) :K
Fallback Calculations
Python functions may have dependencies that aren’t always available, or may take
a long time to run. You can specify a fallback calculation using the ?
operator. If the Python function fails (e.g., missing dependencies, runtime
errors), Oneil will use the fallback and warn the user:
Temperature: T = expensive_simulation(D) ? D * 0.5 + 273 :K
In this example, if expensive_simulation fails, Oneil will calculate
D * 0.5 + 273 instead and display a warning that the Python function should be
run for greater accuracy.
This is particularly useful for:
- Sharing models with users who may not have all Python dependencies installed
- Providing quick approximations during iterative design
- Graceful degradation when simulations fail
Function Caching
Note
This is not currently implemented in the Rust implementation but will be implemented soon.
Python function results are automatically cached to avoid re-running expensive calculations. The cache:
- Persists across REPL sessions - Close and reopen Oneil, cached results remain
- Is version-controllable - Stored as one cache file per model under
__oncache__/ - Is human-readable - Each model cache stores the simulation function,
simulation file, and parameter input/output snapshots (
min,max,units) as JSON for cleaner git diffs - Is shareable - Other users can use cached results even without Python dependencies as long as function arguments match the cached arguments
- Only rewrites changed entries - Re-running Oneil leaves the cache file untouched unless a simulation’s latest cached inputs or output changed
- Auto-invalidates when imported Python source files, their local Python dependencies, or the simulation inputs change
Use the cache command in the CLI to view cache statistics or cache clear to clear it.
Appendix A: Python library (import oneil)
The python-lib Cargo feature builds Oneil’s Python extension module (oneil_python::py_compat). It exposes builtin values, units, and functions, plus Python classes for Interval, MeasuredNumber, and Unit.
The Rust CLI enables python-lib by default. When a model imports a .py file, the CLI injects this same module into the machine’s installed Python 3.12, so helpers can import oneil without a pip package.
You can also install the module for a standalone Python process (pip install / maturin) when you want those types outside the CLI. Oneil’s primary interface is still the Rust CLI — see Installation.
Installation
The Python package is built with maturin and the
python-lib feature. From the repository root:
pip install -e .
Or build a wheel:
maturin build --release -f
pip install target/wheels/oneil-*.whl
Version tags may also attach pre-built wheels on the GitHub Releases page.
Requires Python 3.10+.
Module layout
The package is named oneil. It exposes:
| Submodule / class | Description |
|---|---|
oneil.values | Builtin constants (e.g. pi, e) as Python objects |
oneil.functions | Builtin functions (e.g. min, max, sqrt, sin) as callables |
oneil.units | Builtin units (e.g. m, kg, s) as Unit instances, including SI-prefixed variants |
oneil.Interval | Class for closed numeric intervals |
oneil.MeasuredNumber | Class for a number (scalar or interval) with a unit |
oneil.Unit | Class for dimensional units |
oneil.values
Constants matching Oneil’s builtin values. Each is converted to a Python object (float, or other value type as in Oneil).
pi— πe— Euler’s number
Example:
import oneil
# for now, Oneil doesn't support direct imports like the following,
# but it may in the future
#from oneil.values import pi as on_pi, e as on_e
on_pi = oneil.values.pi
on_e = oneil.values.e
print(on_pi, on_e)
oneil.functions
Oneil’s builtin functions as callables. They accept the same conceptual argument types as in Oneil: Python float, oneil.Interval, and oneil.MeasuredNumber. Arguments are converted to Oneil values; on type or unit mismatch they raise TypeError or ValueError.
Supported functions:
min,max— minimum/maximum of numbers (scalars or intervals).sin,cos,tan— trig (radians).asin,acos,atan— inverse trig (result in radians).sqrt— square root.ln,log2,log10— natural, base-2, and base-10 logarithms.floor,ceiling— round down/up to nearest integer.range— with one argument (an interval): max − min; with two arguments: their difference.abs,sign— absolute value and sign.mid— with one argument (an interval): midpoint; with two arguments: midpoint between them.strip— strip units from a measured number, returning the numeric value.mnmx— return both the minimum and maximum of the given values.
Call with positional arguments:
import oneil
# for now, Oneil doesn't support direct imports like the following,
# but it may in the future
#from oneil.functions import sqrt as on_sqrt, min as on_min
on_sqrt = oneil.functions.sqrt
on_min = oneil.functions.min
print(on_sqrt(2.0))
print(on_min(1.0, 2.0, 3.0))
oneil.units
Builtin units as oneil.Unit instances. Names match Oneil’s unit aliases, with two substitutions for valid Python identifiers:
%→percent$→dollar
Units that support SI prefixes (e.g. m, g, s) also have prefixed names (e.g. km, mm, kg, mg, ms).
Examples:
import oneil
from oneil.units import m, kg, seconds, dollar, percent
# prefixed
from oneil.units import km, mm, kg, mg
oneil.Interval
Closed interval of real numbers with a minimum and maximum. Wraps Oneil’s interval type; supports arithmetic and comparison with other Interval instances or with Python scalars (a scalar is treated as a point interval).
oneil.Interval constructor
Interval(min, max)—minandmaxmust not be NaN, andmin≤max; otherwiseValueErroris raised.
oneil.Interval class methods
Interval.empty()— empty interval.Interval.zero()— [0, 0].
oneil.Interval instance methods and properties
min,max— bounds (read-only).is_empty(),is_valid()— emptiness and validity checks.intersection(other)— intersection with anotherInterval.tightest_enclosing_interval(other)— smallest interval containing both.contains(other)— whether this interval contains the other.
Arithmetic and comparison: +, -, *, /, %, **, unary +/-, and ==, !=, <, <=, >, >=. The other operand may be an Interval or a scalar (float).
Math methods (return new Interval): sqrt, ln, log10, log2, abs, sign, sin, cos, tan, asin, acos, atan, floor, ceiling, pow(exponent) (exponent is an Interval).
Specialized (interval) operations:
escaped_sub(other)— subtract using (min−min, max−max).escaped_div(other)— divide using (min/min, max/max).
oneil.MeasuredNumber
A number (scalar or interval) with a unit. Wraps Oneil’s measured number type; arithmetic and comparison enforce dimensional consistency where required.
oneil.MeasuredNumber constructor
MeasuredNumber(value, unit)—valueis afloat, anInterval, or aMeasuredNumber;unitis aUnit. Builds a measured number from that value and unit.
oneil.MeasuredNumber instance methods
unit()— returns theUnitof this measured number.into_number_and_unit()— returns a tuple(number, unit)wherenumberis the numeric part (float orInterval) in this object’s unit.into_number_using_unit(unit)— converts to a number (float orInterval) in the givenUnit; raises if dimensions don’t match.into_unitless_number()— same as converting to a dimensionless unit; raises if not dimensionless.with_unit(unit)— returns a copy with the given unit; raises if not dimensionally equivalent.
Arithmetic: +, -, *, /, %, ** with other MeasuredNumber or, when the measured number is effectively unitless, with plain numbers. Unit mismatches raise ValueError.
Comparison: ==, !=, <, <=, >, >= (with same conversion rules as arithmetic).
Math (return MeasuredNumber): sqrt, ln, log10, log2, abs, floor, ceiling.
Other:
min(),max()— minimum/maximum as measured numbers.min_max(other)— tightest enclosing measured number of this andother.escaped_sub(other),escaped_div(other)— escaped subtraction and division (units must match).
oneil.Unit
Represents a dimensional unit (dimensions, magnitude, optional decibel flag, and display info).
oneil.Unit constructor
Unit(*, dimensions=None, magnitude=None, is_db=None, display_unit)dimensions— optional dict mapping dimension keys to exponents (e.g.{"m": 1, "s": -1}). Valid keys:"kg","m","s","K","A","b","$","mol","cd".magnitude— optional scale (default 1.0).is_db— optional decibel flag (defaultFalse).display_unit— required string used as the display name (single unit, exponent 1).
oneil.Unit class methods
Unit.one()— dimensionless unit 1.
oneil.Unit properties and methods
magnitude,is_db,display_string— magnitude, decibel flag, and display string.get_dimensions()— dict of dimension key → exponent.is_dimensionless()— whether the unit is dimensionless.dimensionally_eq(other)— same dimensions as anotherUnit.dimensions_match(dimensions)— dimensions match the given dict.numerically_eq(other)— same dimensions, magnitude, andis_db.
Arithmetic: *, /, ** (exponent as float) with other Unit instances.
with_is_db_as(is_db)— copy with decibel flag set.mul_magnitude(factor)— copy with magnitude multiplied.pow(exponent)— unit raised to a power.
Value conversion (Python ↔ Oneil)
From Python into Oneil’s value type:
bool→ booleanstr→ stringfloat→ scalar numberoneil.Interval→ interval numberoneil.MeasuredNumber→ measured number
From Oneil to Python:
- Boolean →
bool - String →
str - Scalar number →
float - Interval number →
oneil.Interval - Measured number →
oneil.MeasuredNumber
The builtin functions in oneil.functions use this mapping for their arguments and return values. Passing an unsupported type raises TypeError with a message that includes the received type.
Appendix B: Using AI
Oneil can be used effectively with AI to model and design systems. The following is an example ruleset for Oneil.
---
description: Senior systems engineer with experience in Oneil
globs: *.on, *.one
alwaysApply: true
---
# Oneil Development Rules
You are an experienced systems engineer who methodically segments and designs
complex physical systems. Follow these modeling principles:
- Do not use magic numbers. Show your work or sources and clarify assumptions.
- Subdivide models into logical hierarchical subsystems. Align a subsystem with
a specific hardware component when that component stands alone. Model
functionality shared by multiple subsystems in a system model.
- Model only what is required to calculate performance metrics.
- Model from the bottom up: specify design inputs and calculate performance
outputs. Independent parameters should generally represent design choices
that the engineer directly controls.
- Maintain one source of truth for each physical property or relationship.
Oneil's language and syntax change frequently. Before writing Oneil code, review
[Oneil documentation](https://oneil-lang.org),
[coding standards](https://raw.githubusercontent.com/careweather/oneil/refs/heads/main/docs/ONEIL_CODING_STANDARDS.md),
and relevant `.on` and `.one` examples. Do not rely on remembered syntax.
Adhere to these Oneil coding standards:
- Mark performance parameters by prepending the parameter line with `$ `. Every
performance parameter should have at least one test that references it.
- Use sentence case for parameter names.
- Keep parameter IDs short and simple. Prefer short subscripts and do not use
multiple subscripts (`v_wmx`, not `v_wind_max`). Because imported model names
distinguish their parameters, a parameter inside a submodel often needs no
subscript (`V`, not `V_b`).
- Write clear notes that explain how equations were derived or values obtained.
Cite relevant URLs or publications without restating the parameter's name,
equation, value, or units.
- Write notes in Markdown. Use LaTeX only for inline equations, standard
Markdown links for URLs, and Pandoc-style citations for references.
- Put a shared source in the model's introductory note and refer back to it from
parameter notes, or use citations, rather than repeating the same URL.
- Use `{{ID:equation}}` and `{{ID:value}}` interpolation when a note must show a
live equation or computed value. Summary tables are a common use; generally
do not interpolate a parameter into its own note.
- Treat units as built-in types. Do not put units in parameter names, IDs, or
notes, and do not convert units manually. Preserve the units used by the
source; for example, use `Length: L = 18 :in`, not
`Length: L = 18*.254 :m`.
- Structure low-level models around hardware. Represent an interchangeable
component with a general model file and put a specific component's values in
a design file. A model dedicated to one non-interchangeable component may use
its model number as the filename.
- Put universally true facts in `constants.on` and import that file as a
reference, not as a submodel.
- Use limits as sanity checks for real-world values and tests for relationships
between parameters.
- Use Oneil's interval arithmetic instead of separate minimum and maximum
parameters when one interval can represent both edge cases.
- Reference a parameter from another model as `<parameter>.<model_ref>`; for
example, reference `V` from `battery` as `V.battery`.
Appendix C: Continuous Integration
Once your models declare test: checks, run them in CI the same way you run them locally. This appendix is for model repositories (repos that contain .on / .one files).
Pin Action refs and Oneil versions to the same release tag (for example v1.0.0) so CI does not silently move under you.
Recommended: model-test-report
Use careweather/oneil/actions/model-test-report as the default CI integration. It installs a released Oneil CLI (via install-oneil), runs oneil test --format json on discovered models, writes a Markdown report to the job summary, and can diff a PR head against its base so the report highlights regressions and fixes rather than only a raw pass/fail count. No Rust toolchain is required.
Single checkout (push / PR)
name: Oneil model tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: careweather/oneil/actions/model-test-report@v1.0.0
with:
oneil-ref: v1.0.0
model-dir: model
Compare a PR against its base
name: Oneil model test report
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/checkout@v4
with:
path: head
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
- uses: careweather/oneil/actions/model-test-report@v1.0.0
id: report
with:
oneil-ref: v1.0.0
head-dir: head
base-dir: base
model-dir: model
head-label: ${{ github.event.pull_request.head.ref }}
base-label: ${{ github.event.pull_request.base.ref }}
report-path: oneil-test-report.md
- uses: actions/upload-artifact@v4
if: always()
with:
name: oneil-test-report
path: ${{ steps.report.outputs.report-path }}
With base-dir set, the Action fails the job when there are regressions, new failures, or new diagnostics (not merely because the base branch already had failing tests). Outputs include has-problems, report (Markdown), and report-path for posting a PR comment or uploading an artifact.
Useful inputs
| Input | Purpose |
|---|---|
oneil-ref | Release tag to install (required), e.g. v1.0.0 |
model-dir | Directory of .on / .one files (default model) |
models | Explicit comma-separated file list (skips auto-discovery) |
skip-models | Files to exclude from auto-discovery |
timeout-seconds | Per-model timeout (default 120) |
fail-on-problems | Set false to report without failing the job |
report-path | Also write the Markdown report to a file |
Auto-discovery only considers top-level .on / .one files in model-dir that declare at least one test: block. Submodel tests reached via imports are covered when the Action runs oneil test --recursive on those entry points. Design files (.one) that declare their own tests are included.
Full reference: Action README.
Install the CLI: install-oneil
When you need oneil on PATH for custom steps (or a minimal workflow of your own), use careweather/oneil/actions/install-oneil directly. model-test-report already uses it internally.
name: Oneil model tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: careweather/oneil/actions/install-oneil@v1.0.0
with:
version: v1.0.0
- run: oneil test --recursive model/radar.on
oneil test exits with status 1 if there were any error diagnostics or any failing test, and 0 otherwise. Use --format json when a later step will parse the report.
| Input | Purpose |
|---|---|
version | Release tag to install (required), e.g. v1.0.0 |
github-token | Optional; defaults to the job token |
Outputs: version (from oneil --version) and oneil-path.
Full reference: Action README.
Tips
- Prefer
model-test-reportfor model repos. Useinstall-oneilwhen you need a plain CLI for scripts or a hand-rolled test loop. - Pin versions. Treat Oneil like a compiler: bump Action tags and
oneil-ref/versiontogether when you adopt a new release. - Keep tests close to requirements. CI is most useful when
test:lines encode margins and constraints you care about — see Tests. - Python models. If imports need packages, install them in the workflow before the Action (or before
oneil test). - Other CI systems. Download a release binary from Releases (see Installation), then run
oneil test --recursive …and rely on the exit code.