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 repetetive 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. 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

It is also possible to import other models as “submodels” using the syntax use <submodel_file> as <submodel_identifier>. Parameters from within that submodel can be referenced with the syntax <variable_name>.<submodel_name>.

# satellite.on
use battery
use magnetometer as m
use radar as r

Satellite peak power: P_max = P_max.m + P_max.r :W

$ Instantaneous battery usage: U_B = P_max/load_max.b :%
# 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 of a model correlates to a subsystem of the system being modeled. When you need to reference variables within a model but don’t want to treat it as a submodel, use ref <model_file> as <model_name> instead.

# orbit.on
ref 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

Note

This is not supported yet, so this section is incomplete. But it will be supported soon!

A design allows you to change certain parameters of a model in order to represent a similar system.

Installation

This section describes how to install the Oneil CLI (Rust implementation) on Linux, Windows, and macOS. Pre-built binaries are provided for these platforms via GitHub Releases.

Prerequisites

  • Rust (for building from source): 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

Optional, for full functionality:

  • Python 3.10+ with pip (for Python functions and optional runtime features). The CLI can run without it; Python is only needed when your models import Python-defined functions.
  • Python development libraries
    • Install on Fedora/RHEL: sudo dnf install python3-devel
    • Install on Debian/Ubuntu: sudo apt install python3-dev

Option 1: Download a release from GitHub (NOT AVAILABLE YET)

Pre-built binaries are published on the Releases page for:

  • Linux (x86_64, unknown-linux-gnu)
  • Windows (x86_64, pc-windows-msvc)
  • macOS (x86_64 and Apple Silicon, apple-darwin)

Linux / macOS

  1. Open the latest release.

  2. Download the archive for your OS and architecture (e.g. oneil-v0.16.0-x86_64-unknown-linux-gnu.tar.gz).

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

    tar -xzf oneil-v*-x86_64-unknown-linux-gnu.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
    

    On macOS, use the appropriate archive (e.g. oneil-v*-aarch64-apple-darwin.tar.gz for Apple Silicon).

  4. Confirm:

    oneil --version
    

Windows

  1. Open the latest release.

  2. Download the .zip for Windows (e.g. oneil-v0.16.0-x86_64-pc-windows-msvc.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: Install from source using the install script

From the repository root, the install script checks for Cargo, then installs the Rust CLI (cargo install) and, by default, the Python package (pip install) so you can run oneil and import oneil.

git clone https://github.com/careweather/oneil.git
cd oneil
./install.sh
  • Without Python (CLI only, no bindings and no pip package): ./install.sh --no-python
  • Editable Python install: ./install.sh --editable or ./install.sh -e

On Windows, run install.bat from the repository root with the same flags (--no-python, -e, --help).

For the default install you also need Python 3.10+ with pip and the development libraries.

Option 3: 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-rs/oneil
    

    This places the oneil executable in ~/.cargo/bin (or %USERPROFILE%\.cargo\bin on Windows). Ensure that directory is on your PATH.

    Optional: build without Python support (avoids Python/pyo3 dependencies, makes smaller binary):

    cargo install --path src-rs/oneil --no-default-features --features rust-lib
    
  3. Confirm:

    oneil --version
    

Option 4: 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

Currently, there is no dedicated way to update Oneil. If you installed from source, update the source code with git, then re-install Oneil. If you downloaded a release from GitHub, download the new version and replace the previous oneil binary with the new one.

Editor and tooling (optional)

  • Vim (currently unmaintained): See the Vim support section in the main README for syntax highlighting.

  • VS Code / Cursor: Install the Oneil extension from the Marketplace for LSP and syntax highlighting.

Install Oneil Python library

To install the oneil package into your current Python environment from a checkout of the repository, run pip install . from the project root (the directory that contains pyproject.toml):

git clone https://github.com/careweather/oneil.git
cd oneil
pip install .

After installation you can import oneil in Python.

Note

pip install . alone does not install the CLI. Use Option 2 (./install.sh) or Option 3 (cargo install --path src-rs/oneil) if you want both the CLI and the library.

Uninstalling Oneil

If Oneil was installed as a release binary, delete the release binary.

If Oneil was installed from source, run cargo uninstall oneil.

If Oneil’s Python library was installed, run pip uninstall oneil in the same virtual environment that it was installed in.

Troubleshooting

  • oneil: command not found
    Ensure the directory containing the oneil binary is on your PATH.

  • Python-related build errors (from source)
    Either install Python 3.10+ and development headers, or install with --no-default-features --features rust-lib to disable Python support.

  • Permission denied (Linux/macOS)
    After moving the binary, run chmod +x /path/to/oneil (or the path you used).

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

For example,

Number of batteries: count = 10

Here, Number of batteries is the label, count is the name, and 10 is the value.

A diagram of the parts of a parameter

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

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 inline LaTeX, called “notes”. It’s like a built-in documentation system.

Parameter Notes

To add a note to a parameter, start the following line with the ~ character.

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

Importing models

One of the purposes of Oneil’s models is to be able to represent collections of systems and subsystems. To this end, Oneil provides two different ways to import a model.

The first way to import a model is as a reference. When a model is imported as a reference, all of the reference model parameters are made available through the reference alias. The reference alias is either the alias provided or, if there isn’t one, the name of the model.

# constants.on
Gravity of Earth: g = 9.8 :m/s^2
# box.on
Mass of box: m_b = 5 :kg

# reference with alias
ref constants as c
Weight of box: w_b = m_b * g.c :N
# box2.on
Mass of box: m_b = 5 :kg

# reference without alias
ref constants
Weight of box: w_b = m_b * g.constants :N

The second way to import a model is as a submodel. Like with a reference, all of the submodel parameters are available through the submodel alias. In addition to this, the model is also exported as a submodel of the current model. This means that the imported model can be referenced as model.submodel.

# radar.on
Radar cost: cost = 1000 :$
# solar_panel.on
Solar panel cost: cost = 500 :$
# satellite.on
use radar
use solar_panel as solar

Satellite cost: cost = cost.radar + cost.solar :$
# product.on
use satellite
ref satellite.radar
ref satellite.solar_panel as solar
# ... or using `with` syntax ...
use satellite with [radar, solar_panel as solar]

Note that in the case of a submodel, the submodel and reference name may be different. If an alias is provided, it will be used as the reference name, but not as the submodel name. The submodel name will always be the name of the model.

Importing Python Functions

For functions not supported by the above equation formats, you can define a python function and link it.

Functions are stored in a separate python file, which must be imported in the Oneil file.

import <name of functions file>

That file should simply define functions matching the name used in the parameter:

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 shareable - Other users can use cached results even without Python dependencies
  • 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
  • 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 API (oneil)

The Oneil Rust implementation exposes a Python library built from oneil_python::py_compat. It provides Oneil’s builtin values, units, and functions, plus Python classes for Interval, MeasuredNumber, and Unit, so you can use Oneil’s number and unit semantics from Python.

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

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 minmax; 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. As an experienced systems engineer, you are methodical in your approach to segmenting and designing complex physical systems. You follow best practices, like:

- Do not use magic numbers. Always show your work or your sources. Clarify your assumptions.
- Subdivide models into logical heirarchal subsystems. You should typically align these subsystems with a specific hardware component if it stands by itself. If a functionality is filled collaboratively by multiple subsystems, it should be modeled in a top-level system model.
- Only model what is required to calculate performance metrics. Don't include superfluous modeling. Think carefully about all of the considerations that affect the performance metrics.
- Model from the bottom up. Specify the design inputs and calculate the performance output, not the other way around. Independent parameters (those that are assigned a value instead of equation) should generally be design parameters that the engineer has more direct control over.
- Do not duplicate parameters. There should be one source of truth for each physical property or relationship. If this is not possible for some reason, use comments to make clear that this is a duplicate parameter.

To model your systems, you use a new specification language, called Oneil. While you are an expert in Oneil, the language and its syntax is frequently updated, so you don't assume you inherently know how to write good Oneil code. Instead you re-read the [Oneil documentation](https://github.com/careweather/oneil) and these instructions before each time you write Oneil code to make sure your code is up-to-date with the latest syntax and best practice. You also review many other Oneil files for syntax and best practice examples in @/home/patrick/careweather/nest/model and @/home/patrick/careweather/veery/model.

Adhere to the following best practices in Oneil:

- Mark performance metrics by prepending the parameter line with "$ ". See other model files for examples of top-level metrics.
- Be very clear in the note that follows the parameter. Provide a description of how you derived the equation or obtained a value. Provide sources where relevant, either URLs or journal references. But do not repeat yourself. For example, if the parameter name is "Flux capacitor power consumption", don't say in the note "This is the power consumption of the flux capacitor", instead say, "taken from the Doc's own Delorean handbook, page 13."
- Parameter names should use sentence case.
- You should write your notes in LaTeX. This means if you give a URL in a note, you should use \href, and if you use special LaTeX characters like % and &, you need to escape them.
- If multiple parameters would give the same URL as a source, consider including that source in the introductory note and referencing in the parameter notes. For example, if this is an off-the-shelf electronic component, the introductory note would give the source for the datasheet and the parameter notes could just say something like, "given on page # of the datasheet."
- Your parameter IDs should be as simple as possible. Prefer short subscripts and never use multiple subscripts (v_wmx instead of v_wind_max).
- It's generally better to structure your submodels around actual hardware, at least the lowest-level models, because then you can have a model file that's tied to the specifications and properties of one component. For example, if you have a solar.on file which represents a solar power system, it could import a SM500K12L.on, which represents a specific solar cell component that can be purchased off the shelf. If a Oneil file refers specifically to an off-the-shelf component, it is preferable to name the file after using the component model number.
- If a parameter is a fact that is generally true regardless of the component or design, include it in a constants.on file and import it. For example, the speed of light, should go in constants.on.
- Oneil treats units as built-in types. You don't need to specify units anywhere else. Do not specify units as a subscript to the ID, as part of the name, or in the note. Do not convert units manually. Doing so will result in duplicate conversion errors.
- Oneil should handle all units that the user might specify. Always specify units as cited in the source. For example, if the length of an object is given as 18 inches, use `Length: L = 18 :in`, not `Length: L = 18*.254 :m`. If you get an error for an unsupported unit, you may convert the specified unit and note the original. However, in this case, you should let the user know that the unsupported unit needs to be added.
- IDs are used to produce typeset equations. The shorter the name the better. For example, battery voltage, should use "V_b" instead of "V_batt".
- Also in typesetting, imported submodels are given as a superscript. If the battery voltage appears in the battery submodel, then it should have no subscript at all, just "V".
- Oneil has built in formal verification in two forms. Do not mix them up. Review your designs for potential bounds you should clarify.
  1. You can specify bounds on any parameter. The default is (0, inf), but in some cases another bound may be appropriate. For example, if calculating an efficiency, only values in the range (0, 1) are valid. Alternatively, if calculating a net energy generation, values in the range of (-inf, inf) would be valid.
  2. You can specify tests for relational limits. For example, let's say you are designing a smartphone. You specify the battery capacity, "C_b", and the model calculates the corresponding battery volume, "V_b". You could use a relational test to make sure the battery volume is not larger than the total smartphone volume, "V": `test : V_b < V`.
- Don't repeat yourself. For Oneil, name, ID, math, units, and sources/notes all have their own place. Don't put units in the name, ID, or note. Don't re-state the name in the note. Don't re-state the math in the note, unless you derive it in more detail there.
- Oneil supports built-in interval arithmetic, never make separate minimum and maximum parameters when you can make one parameter and specify the minimum and maximum edge cases.
- References to parameters in external models are always `<parameter>.<model_ref>`. For example, if I have a variable `V` in submodel `battery`, I would reference it as `V.battery`.