Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.12 on Linux, or Python 3.12 on PATH on Windows
  • uv — uv python install 3.12. The extension sets the library search path when it launches the CLI. For a PATH install, 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:

  1. Open the latest release.

  2. Download the archive for your OS, architecture, and Python layout (for example oneil-v1.0.0-x86_64-unknown-linux-gnu-system.tar.gz or oneil-v1.0.0-aarch64-apple-darwin-homebrew.tar.gz).

  3. Unpack and put the oneil binary on your PATH:

    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
    
  4. Confirm:

    oneil --version
    

Windows

  1. Open the latest release.

  2. Download the Windows zip for your Python layout (for example oneil-v1.0.0-x86_64-pc-windows-msvc-system.zip).

  3. Unzip and either move oneil.exe into a directory on your PATH, or add the folder containing oneil.exe to your PATH.

  4. 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 cargo is on your PATH.

  • gcc

    • Install on Fedora/RHEL: sudo dnf install gcc
    • Install on Debian/Ubuntu: sudo apt install build-essential
  • Python 3.12 — the only CPython version Oneil supports. Needed at runtime when models import Python 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 .py files can import oneil because the CLI includes the Python library.

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.

  1. Clone the repository:

    git clone https://github.com/careweather/oneil.git
    cd oneil
    
  2. Build and install the oneil binary (requires Rust):

    cargo install --path src/oneil
    

    It places oneil in ~/.cargo/bin (or %USERPROFILE%\.cargo\bin on Windows); keep that directory on your PATH.

    Building from source requires Python 3.12 (see Prerequisites).

  3. 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 oneil binary on your PATH.
  • Nix: bump the oneil flake input in your configuration and rebuild.
  • From source: pull the latest code (or check out the new tag), then re-run ./install.sh or cargo 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.oneil from 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. Set oneil.serverPath only when you want to force a local build; that setting disables managed updates. The Nix package already sets oneil.serverPath to 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 the oneil binary is on your PATH.

  • Python-related build errors (from source) or oneil --version aborts
    Install Python 3.12 to match the archive flavor you downloaded (brew install python@3.12, the python.org 3.12 installer, distro python3.12, or uv python install 3.12). See Prerequisites. The extension picks the flavor for you.

  • Permission denied (Linux/macOS)
    After moving the binary, run chmod +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:

  1. Name - A human-readable name (can include spaces). This can contain any character except the following: (, ), [, ], {, }, #, ~, :, =, \n, *, and $.

  2. 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 (_).

  3. 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:

AnnotationSymbolMeaning
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 AND
  • or - 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 :s

To 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, run oneil 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*K is treated as (J/kg)*K rather than J/(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.

OperationInput RulesUnit Output
a + b, a - b, a % ba_unit and b_unit must have the same dimensionsa_unit
a * bNonea_unit * b_unit
a / bNonea_unit / b_unit
a ^ bb cannot have any dimensionsa_unit ^ b
comparison (<, >, <=, >=, ==, !=)a_unit and b_unit must have the same dimensionsN/A (produces true or false)

Examples

Note

empty.on is 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.on is 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 //:

OperatorEquivalent to
a -- bmin(a) - min(b) | max(a) - max(b)
a // bmin(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:

OperatorEquivalent toDescription
a == bmin(a) == min(b) and max(a) == max(b)The min and the max are the same
a != bmin(a) != min(b) or max(a) != max(b)The min or the max is not the same
a < bmax(a) < min(b)The max value of a is less than the min value of b
a <= bmax(a) <= min(b)The max value of a is less than or equal to the min value of b
a > bmin(a) > max(b)The min value of a is greater than the max value of b
a >= bmin(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

AttachmentPlacement
ModelAt the top of the file (before parameters)
ParameterImmediately after the parameter declaration
SectionImmediately after a section Label line
TestImmediately 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).

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 ![alt](./path.png) (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}}:

PlaceholderMeaning
{{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)

SyntaxRenders 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-15Open / 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)

SyntaxRenders as
@keyAuthor (year)
+@key / -@key / !@keyFull 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:

  1. references.bib in the same directory as the open .on / .one file
  2. Other *.bib files in that directory
  3. references.bib at each workspace folder root
  4. Remaining *.bib files anywhere in the workspace (shallower paths first; node_modules skipped)

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

  1. Open the model in Oneil: Open Rendered View.
  2. Click the citation.
  3. When prompted, choose Download & Cache. The file is stored under the PDF cache directory (default ~/.local/oneil/resources/, override with oneil.pdf.cacheDir).
  4. When prompted, choose Update references.bib. The extension writes a portable file field, 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:

  1. BibTeX file (absolute, ~/…, ./… relative to the model file then the workspace root, or bare name in the PDF cache)
  2. Cache entry already downloaded for that URL
  3. Download from url when the response is a PDF (unless offline), then optionally update references.bib
  4. Open url, or https://doi.org/<doi> if there is no url, in the browser

Useful BibTeX fields

FieldRole
author, title, yearCitation labels and bibliography list
urlDirect PDF link (recommended). Used to download into the cache
doiBare identifier, e.g. 10.1088/1681-7575/ac0240. Opens the publisher page; does not cache a PDF
fileLocal or cache path (:filename.pdf:PDF, ./relative.pdf, or absolute)
pdfpageDefault 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.property convention 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 design target is that submodel’s base model.
  • Rule of thumb: import shared environmental data (constants, tables, etc) as reference. Import components and system elements as submodel.

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 / classDescription
oneil.valuesBuiltin constants (e.g. pi, e) as Python objects
oneil.functionsBuiltin functions (e.g. min, max, sqrt, sin) as callables
oneil.unitsBuiltin units (e.g. m, kg, s) as Unit instances, including SI-prefixed variants
oneil.IntervalClass for closed numeric intervals
oneil.MeasuredNumberClass for a number (scalar or interval) with a unit
oneil.UnitClass 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) — min and max must not be NaN, and min ≤ max; otherwise ValueError is 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 another Interval.
  • 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) — value is a float, an Interval, or a MeasuredNumber; unit is a Unit. Builds a measured number from that value and unit.

oneil.MeasuredNumber instance methods

  • unit() — returns the Unit of this measured number.
  • into_number_and_unit() — returns a tuple (number, unit) where number is the numeric part (float or Interval) in this object’s unit.
  • into_number_using_unit(unit) — converts to a number (float or Interval) in the given Unit; 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 and other.
  • 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 (default False).
    • 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 another Unit.
  • dimensions_match(dimensions) — dimensions match the given dict.
  • numerically_eq(other) — same dimensions, magnitude, and is_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 → boolean
  • str → string
  • float → scalar number
  • oneil.Interval → interval number
  • oneil.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.

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

InputPurpose
oneil-refRelease tag to install (required), e.g. v1.0.0
model-dirDirectory of .on / .one files (default model)
modelsExplicit comma-separated file list (skips auto-discovery)
skip-modelsFiles to exclude from auto-discovery
timeout-secondsPer-model timeout (default 120)
fail-on-problemsSet false to report without failing the job
report-pathAlso 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.

InputPurpose
versionRelease tag to install (required), e.g. v1.0.0
github-tokenOptional; defaults to the job token

Outputs: version (from oneil --version) and oneil-path.

Full reference: Action README.

Tips

  • Prefer model-test-report for model repos. Use install-oneil when 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 / version together 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.