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
cargois on yourPATH. - gcc
- Install on Fedora/RHEL:
sudo dnf install gcc - Install on Debian/Ubuntu:
sudo apt install build-essential
- Install on Fedora/RHEL:
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
- Install on Fedora/RHEL:
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
-
Open the latest release.
-
Download the archive for your OS and architecture (e.g.
oneil-v0.16.0-x86_64-unknown-linux-gnu.tar.gz). -
Unpack and put the
oneilbinary on yourPATH: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 PATHOn macOS, use the appropriate archive (e.g.
oneil-v*-aarch64-apple-darwin.tar.gzfor Apple Silicon). -
Confirm:
oneil --version
Windows
-
Open the latest release.
-
Download the
.zipfor Windows (e.g.oneil-v0.16.0-x86_64-pc-windows-msvc.zip). -
Unzip and either move
oneil.exeinto a directory on yourPATH, or add the folder containingoneil.exeto yourPATH. -
Confirm in PowerShell or Command Prompt:
oneil --version
Option 2: 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 --editableor./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.
-
Clone the repository:
git clone https://github.com/careweather/oneil.git cd oneil -
Build and install the
oneilbinary (requires Rust):cargo install --path src-rs/oneilThis places the
oneilexecutable in~/.cargo/bin(or%USERPROFILE%\.cargo\binon Windows). Ensure that directory is on yourPATH.Optional: build without Python support (avoids Python/pyo3 dependencies, makes smaller binary):
cargo install --path src-rs/oneil --no-default-features --features rust-lib -
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 theoneilbinary is on yourPATH. -
Python-related build errors (from source)
Either install Python 3.10+ and development headers, or install with--no-default-features --features rust-libto disable Python support. -
Permission denied (Linux/macOS)
After moving the binary, runchmod +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:
-
Name - A human-readable name (can include spaces). This can contain any character except the following:
(,),[,],{,},#,~,:,=,\n,*, and$. -
Identifier - The identifier used in expressions (e.g.
x). It must appear after the colon and before=. Other parameters and expressions refer to the parameter by this identifier. A name starts with a letter and may contain letters, digits, and underscores (_). -
Expression - The expression on the right-hand side of
=. It can be a number, a reference to another parameter, or a more complex expression (with optional unit; see Value Types and Units).
The syntax is:
Name: identifier = expression
For example,
Number of batteries: count = 10
Here, Number of batteries is the label, count is the name, and 10 is the
value.
Comments
Comments in Oneil are prefixed by # and go until the end of the line.
# this is a comment
My param: p = 10
My other param: o = 5 # comments don't have to start at the beginning of a line
Running a model and viewing output
To evaluate a model file, use:
oneil eval <model>.on
For example:
# antenna.on
Antenna length: a_l = 5
oneil eval antenna.on
(No performance parameters found)
Note that there are no parameters printed out. The reason for this is discussed in Annotations.
For now, use --print all to print out all parameters.
oneil eval antenna.on --print all
a_l = 5 # Antenna length
Note
Throughout this guide, we will insert a comment at the top of each model indicating the name of the model.
# my_model.on ...This way, we can reference it when running
oneil eval.oneil eval my_model.on
Multiple parameters and references
You can define multiple parameters in one file. They can reference each other by name, and the order of declarations does not matter - Oneil resolves dependencies automatically.
For example,
# satellite.on
Body length: l_body = 25
Antenna length: l_a = 5
Satellite length: l_sat = l_body + l_a
oneil eval satellite.on --print all
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
Selecting parameters to print
By default, only parameters with certain annotations are printed. To print specific parameters by name, use --params (or -p):
oneil eval <model>.on --params param1,param2
# or
oneil eval <model>.on -p param1,param2
For example,
oneil eval satellite.on -p l_body,l_a
l_body = 25 # Body length
l_a = 5 # Antenna length
The order in the comma-separated list is the order they appear in the output. You can select one or more parameters; only those are printed.
Annotations
Parameters can be marked with optional annotations that control whether they are printed by default and how they are used:
| Annotation | Symbol | Meaning |
|---|---|---|
| Trace | * | Included when printing “trace” parameters (default). |
| Debug | ** | Same as trace, and with --debug / -D, extra debug info is printed for variables used to evaluate this parameter. |
| Performance | $ | Marked as a performance variable. |
Annotations appear before the label.
# satellite2.on
# No annotation
Mass: m = 10
# Trace annotation:
* Body length: l_body = 25
## Debug annotation:
** Antenna length: l_a = 5
# Performance annotation:
$ Satellite length: l_sat = l_body + l_a
oneil eval satellite2.on
l_sat = 30 # Satellite length
With the default print mode (perf), only the performance variables are displayed.
To display the other annotated variables, use the --print/-P argument with the trace argument.
oneil eval <model>.on --print trace
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
To display all variables, including non-annotated variables, use --print all.
oneil eval <model>.on --print all
m = 10 # Mass
l_body = 25 # Body length
l_a = 5 # Antenna length
l_sat = 30 # Satellite length
Evaluating expressions
While it is usually best to include all equations in the model itself, there may
be some times when you need to do a quick evaluation of an expression. For that,
Oneil provides the --expr/-x argument. This argument prints out the result
of evaluating the provided expression. The expression can include parameters
from the model.
# orbit.on
Orbit radius: r = 7000
# determine the orbit circumference
oneil eval orbit.on --expr "2*pi*r"
2*pi*r = 43982
In addition, you can provide multiple expressions at the same time.
# determine the orbit circumference and diameter
oneil eval orbit.on --expr "2*pi*r" --expr "2*r"
# or, using the shorter argument name
oneil eval orbit.on -x "2*pi*r" -x "2*r"
pi*r^2 = 1.539e8
2*r = 14000
Value Types
Parameter values can include literals, equations, and imported functions. The main literal types are numbers, strings, and booleans.
Numbers
Numbers use familiar decimal notation: whole numbers, values with a decimal point, and scientific notation (e or E) when a value is very large or very small. inf can be used for infinity.
For example,
100th prime number: p_100 = 541
Golden ratio: phi = 1.618
Avogadro constant: N_A = 6.022e23
Infinity: inf_val = inf
Number Operators
Arithmetic (for ordinary scalar values):
^- exponentiation*- multiplication/- division%- modulo+- addition-- subtraction
Comparisons (produce booleans; can be chained, e.g. 1 < 2 < 3):
<- less than>- greater than<=- less than or equal>=- greater than or equal==- equal!=- not equal
Built-in pi and e
The identifiers pi and e are built-in numeric constants (π and Euler’s number). They can be used like any other value in expressions.
Strings
Strings behave like fixed strings, not like growable text in many other languages. Typical uses include modes or categories. For example, a battery’s array configuration could be either 'series' or 'parallel'. As another example, a remote sensing resolution mode might be 'polar', 'track', or 'footprint'.
A string is written in single quotes '...'. Double quotes are not used for strings.
String Operators
There is no concatenation or mutation. Strings can only be compared for equality:
==- equal!=- not equal
String Examples
# battery.on
Battery configuration: config = 'series'
oneil eval battery.on \
-x "config == 'series'" \
-x "config == 'array'"
config == 'series' = true
config == 'array' = false
Booleans
Boolean literals are the keywords true and false.
Boolean Operators
not- logical NOT (unary)and- logical ANDor- logical OR
Boolean Examples
Comparisons on numbers (and string equality) also yield booleans. Booleans are used in piecewise parameter conditions and in test declarations; those topics appear in later chapters.
Units
One of Oneil’s defining features is its unit-based type system. Oneil tracks units, disallows invalid operations between different physical properties, and automatically converts between differing units of the same physical property.
For example, Oneil will throw an error if you try to add a time and a distance or compare a mass and a temperature. But it will automatically convert a length in meters and a length in feet to a common base before adding them together.
This simplifies expressions to focus on relationships between physical properties while preventing unit conversion errors that might crash your spacecraft.
Units, dimensions, and magnitude
Before we get into how units work in Oneil, we’re going to take a quick detour to delve into what makes units compatible. Why can you add meters and kilometers, but not meters and kilograms? Why is a Joule equivalent to a Watt-second?
The answer is dimensions. A dimension can be defined as an aspect of something that can be measured. That definition is hard to understand on its own, though, so lets consider the dimension of time as an example.
A trip to the store (and more)
When measuring how long a car takes to get from your house to the store, it doesn’t matter whether you measure in seconds, minutes, or even millennia. They all are measurements of the dimension of time.
You can also measure how long it takes for you to get from the store to work, and you can measure that in any unit of time as well. Then, you could add the values together because they both measure the dimension of time.
However, it wouldn’t make sense to add the mass of your car to the measured travel time, since travel time is measured in the dimension of time, while car mass is measured in the dimension of mass.
Supported dimensions
Oneil supports the following dimensions, listed here with their associated base unit.
- mass:
kilogram - distance:
meter - time:
second - temperature:
Kelvin - current:
ampere - information:
bit - currency:
$(USD) - substance:
mole - luminous intensity:
candela
Base units convey a single dimension, like the unit kilometer with its
dimension distance. Derived units convey 0 to many dimensions, like the unit
degree which is dimensionless or the unit Joule with its dimensions of of
mass, distance^2, and time^-2. Dimensionless units are discussed in more
detail later in this chapter.
Each unit has 0 or more dimensions associated with it. The kilometer is defined
as having a dimension of distance, while a Joule would have the dimensions
of mass, distance^2, and time^-2.
There are also dimensionless units such %. These are discussed
later in this chapter.
If you haven’t quite wrapped your head around dimensions yet, don’t worry. You don’t need to fully understand it to use Oneil.
Magnitudes
So if kilometers and millimeters are the same dimensions, then what makes
them different? The difference is in the magnitudes.
A magnitude is the relative size of a unit compared to the base unit. Relative
to the base unit of meters, kilometers has a magnitude of 1000 since
1 km == 1000 m. Meanwhile, millimeters has a magnitude of 0.001 because
1 mm == 0.001 m.
Oneil tracks magnitudes and performs automatic conversions to handle units with
different magnitudes. So when Oneil sees 1 m + 1 km, it knows that it needs
to convert 1 km to 1000 m before adding. The result would therefore end up
being 1001 m.
This automatic conversion also applies to units such as feet and meters, as
well as more complex units like ft*lb/s^2 and Newtons, which could save your
climate orbiter from
a devastating crash.
Assigning units
Now that we’ve reviewed the motivation behind tracking units, let’s get into
the practical application. For parameters with a literal value, units can be
assigned with the :<unit> syntax:
# velocity.on
Distance: d = 100 :meters
Travel time: t = 20 :seconds
This will assign d to a value of 100 :meters and t to a value of
20 :seconds. These values are now measured numbers, or numbers with units.
Note
There are often multiple synonyms for a given unit. For example, the above model could also be written as
Distance: d = 100 :m Travel time: t = 20 :sTo see a list of all builtin units and their synonyms, run
oneil builtins unit. Also, if you would like to search for a given unit, runoneil builtins unit <unit>.
Annotating expected units
For calculated parameters, the :<unit> syntax declares the expected units of
a calculation, which Oneil checks.
For example, using d and t from the previous section, we can then define
velocity as
# velocity.on (continued)
$ Velocity: v = d/t :m/s
defining a parameter with a measured value with the units m/s.
Running the model with oneil eval velocity.on produces
v = 5 :m/s # Velocity
If we wanted to, we could just as easily define velocity in kilometers per hour.
$ Velocity: v = d/t :km/hr
# ^^^^^ `km/hr` instead of `m/s`
oneil eval velocity.on
v = 18 :km/hr # Velocity
Note that we did not have to do any conversions. Oneil handles that for us. However, if we try to use incorrect units, Oneil will produce an error.
$ Velocity: v = d/t :kg/hr
# ^^ `kg` instead of `km`
oneil eval velocity.on
error: calculated unit does not match expected unit
--> velocity.on:3:17
|
3 | $ Velocity: v = d/t :kg/hr
| ^--
= note: calculated unit is `meters/seconds` but expected unit is `kg/hr`
In addition, Oneil requires units on any parameters whose calculations are expected to produce a measured value. If we leave out the unit, we get an error.
Likewise, the calculation for a unitless parameter should not have a measured result. In that case, we do leave the units out (see dimensionless values).
$ Velocity: v = d/t
# ^ No unit
oneil eval velocity.on
error: parameter is missing a unit
--> velocity.on:3:17
|
3 | $ Velocity: v = d/t
| ^--
= note: parameter value has unit `meters/seconds`
= help: add a unit annotation `:meters/seconds` to the parameter
Composing units in a unit expression
A unit expression is built from one or more units separated by * or /.
Each unit can be raised to a numeric power with ^, such as s^2.
Unit expressions can also use the literal 1 as a dimensionless unit. This is
used in rates such as 1/s.
Warning
Multiplication and division operate left to right. So
J/kg*Kis treated as(J/kg)*Krather thanJ/(kg*K).To express
J/(kg*K), explicit parentheses are required.
Unit casting
Imagine that you have a test that takes time to start up before it runs. The full time of the test is 5 minutes and start-up time is 10 seconds. To calculate what the actual run time of the test is, you might write the following model.
# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - 10 :min
However, this will produce an error.
oneil eval testing.on
error: expected scalar with unit `min` but found scalar
--> testing.on:2:30
|
4 | Run time: t_r = t_f - 10 :min
| ^-
In other words, Oneil can’t determine whether 10 is supposed to be 10 seconds,
10 minutes, or 10 hours.
The first recommended solution is to create another parameter to hold this “magic number”. You can then define a unit on that parameter.
# testing.on
Full time: t_full = 5 :min
Startup time: t_start = 10 :s
$ Run time: t_run = t_full - t_start :min
oneil eval testing.on
t_run = 4.833 :min # Run time
However, there are some situations where you may just want to label a unitless number with a unit.
To do so, you can use unit casting. Unit casting takes the form of
(<expression> : <unit>). This allows a unitless value to be assigned a unit.
Using this, the model could be rewritten as
# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - (10:s) :min
oneil eval testing.on
t_run = 4.833 :min # Run time
Arithmetic and comparison operators
Arithmetic and comparison operator rules and behavior are defined by the
following table. The unit of a given value x is indicated by x_unit.
| Operation | Input Rules | Unit Output |
|---|---|---|
a + b, a - b, a % b | a_unit and b_unit must have the same dimensions | a_unit |
a * b | None | a_unit * b_unit |
a / b | None | a_unit / b_unit |
a ^ b | b cannot have any dimensions | a_unit ^ b |
comparison (<, >, <=, >=, ==, !=) | a_unit and b_unit must have the same dimensions | N/A (produces true or false) |
Examples
Note
empty.onis just an empty model, since we don’t reference any model parameters.
# addition, subtraction, modulo
oneil eval empty.on \
-x "(1000:m) + (1:km)" \
-x "(1:km) + (1000:m)" \
-x "(5:min) - (30:s)" \
-x "(80:s) % (1:min)"
(1000:m) + (1:km) = 2e3 :m
(1:km) + (1000:m) = 2 :km
(5:min) - (30:s) = 4.5 :min
(80:s) % (1:min) = 20 :s
# multiplication, division, exponentiation
oneil eval empty.on \
-x "(1:m) * (1:s)" \
-x "(1:m) * (1:m)" \
-x "(1:m) * 1" \
-x "(1:m) / (1:s)" \
-x "(1:m) / 1" \
-x "(1:m)^2"
(1:m) * (1:s) = 1 :m*s
(1:m) * (1:m) = 1 :m*m
(1:m) * 1 = 1 :m
(1:m) / (1:s) = 1 :m/s
(1:m) / 1 = 1 :m
(1:m)^2 = 1 :m^2
# comparison
oneil eval empty.on \
-x "(1:kg) < (2000:g)" \
-x "(1:kg) > (1:g)" \
-x "(1:kg) <= (1000:g)" \
-x "(1:kg) >= (900:g)" \
-x "(1:kg) == (1000:g)" \
-x "(1:kg) != (1:g)"
(1:kg) < (2000:g) = true
(1:kg) > (1:g) = true
(1:kg) <= (1000:g) = true
(1:kg) >= (900:g) = true
(1:kg) == (1000:g) = true
(1:kg) != (1:g) = true
strip
In the case that you would like to treat a measured value as unitless, Oneil
provides the strip function. The strip function removes any units from a
value.
# adc.on
ADC bit resolution: S_adc = 10 :b
$ ADC step count: n_adc = 2^(strip(S_adc)-1)
oneil eval adc.on
n_adc = 512 # ADC step count
The places where this should be used are rare and should be treated cautiously
since strip effectively disables unit checking.
In addition, it is important to realize that strip strips the unit that is
currently associated with a value.
# length.on
Length in meters: l_m = 1000 :m
Length in kilometers: l_km = 1 :km
oneil eval length.on \
-x "strip(l_m)" \
-x "strip(l_km)"
strip(l_m) = 1e3
strip(l_km) = 1
For this reason, when using strip, it is recommended that you first cast
the value to the unit that you expect it to be.
oneil eval length.on \
-x "strip((l_m :m))" \
-x "strip((l_km :m))"
strip((l_m :m)) = 1e3
strip((l_km :m)) = 1e3
Non-linear units
On top of linear units, Oneil supports decibel (dB) units. You form a
decibel unit by prefixing dB directly to a built-in unit name, for example
dBmW (decibels relative to one milliwatt) or dBV. The bare name dB
(with no following unit) is also valid; it behaves as a dimensionless logarithmic
unit.
Support for other non-linear units is on the roadmap.
When any unit is specified with prefix dB, Oneil internally converts the
parameter to the corresponding linear value, performs all calculations in linear
terms, and reconverts the value to dB for display. This means that equations
that contain parameters with dB units should use linear math. For example,
when calculating the signal to noise ratio by hand, you might subtract the noise
(dB) from the signal (dB), but in Oneil, you divide the signal by the noise:
# power.on
Noise power: P_n = -100 :dBmW
Signal power: P_s = -90 :dBmW
$ Signal-to-noise ratio: S_N = P_s/P_n
oneil eval power.on
S_N = 10 # Signal-to-noise ratio
Dimensionless units
There are some units that don’t have any dimensions, such as % or ppm (parts
per million). These units are referred to as dimensionless units, and values
with dimensionless units are referred to as dimensionless values.
Unitless equivalence
Dimensionless values can be treated as if they have no unit. The following
demonstrates this with the % unit.
Note
empty.onis just an empty model, since we don’t reference any model parameters.
# `100%` is treated as equal to `1`
oneil eval empty.on \
-x "(100:%) == 1" \
(100:%) == 1 = true
# the `1` is equal to `100%`, not `1%`
oneil eval empty.on \
-x "(100:%) + 1"
(100:%) + 1 = 200 :%
Angular Units
The lack of distinction between dimensionless values and unitless values is
especially important when it comes to units involving radians. The
International System of Units treats radians as dimensionless, and Oneil has
opted to follow this convention. Therefore, all angular units (such as
radians, degrees, and revolutions) are specified in radians. Therefore,
when adding a unitless number to an angular value, the unitless number is
treated as if it is specified in radians.
oneil eval empty.on \
-x "(1:rad) == 1" \
-x "(360:deg) == 2*pi" \
-x "(1:rad) + 1" \
-x "(360:deg) + 2*pi"
(1:rad) == 1 = true
(360:deg) == 2*pi = true
(1:rad) + 1 = 2
(360:deg) + 2*pi = 720 :deg
Hz and rad/s
There is one place where Oneil’s automatic conversions might cause confusion.
That is with the Hz unit. In order to solve the problem described
in this article
and make Hz compatible with rad/s, Oneil defines Hz as
1 Hz == 1 cycle/s == 2*pi rad/s.
Note that both cycles and radians are both dimensionless values, but
1 cycle == 2*pi radians.
# freq.on
Frequency: f = 1 :Hz
oneil eval freq.on \
-x "f" \
-x "(f :cycle/s)" \
-x "(f :rad/s)" \
f = 1 :Hz
(f :cycle/s) = 1 :cycle/s
(f :rad/s) = 6.283 :rad/s
By default, Oneil treats dimensionless values as if they are in radians.
Because of this, anytime you would like dimensionless values to be in cycles,
you need to manually convert from radians to cycles by dividing by 2*pi.
# freq2.on
Frequency: f = 5 :GHz
Speed of light: c = 299792458 :m/s
$ Wavelength: lambda = c/(f/2*pi) :cm
# ^^^^^ Need to divide by 2*pi to convert radians to cycles
oneil eval freq2.on
lambda = 0.6075 :cm # Wavelength
Tests
Alongside parameters, Oneil provides tests, which allows users to verify that certain properties, requirements, and expectations hold.
The syntax for tests is test: <test-expression>.
test: 1 + 1 == 2
Component A Length: L_A = 5 :cm
Component B Length: L_B = 3 :cm
Max Length: L_max = 10 :cm
test: L_A + L_B <= L_max
A test expression can be any expression that produces a boolean (true or
false). For more information, see Booleans
and Number operations.
Examples
The point of a test is to encode a requirement you care about: margins, safety limits, or physical feasibility.
Thrust versus gravity
For a vehicle to accelerate upward, thrust must exceed weight.
Dry mass: m_dry = 420 :kg
Propellant mass: m_prop = 180 :kg
Liftoff mass: m = m_dry + m_prop :kg
Sea-level gravity: g = 9.81 :m/s^2
Liftoff thrust: F_thrust = 7500 :N
test: F_thrust > m * g
Stress and material limit
Keep computed stress below the allowable value derived from yield strength and a safety factor:
Yield strength: sigma_y = 250 :MPa
Safety factor: SF = 2
Allowable stress: sigma_allow = sigma_y / SF :MPa
Working stress: sigma_work = 95 :MPa
test: sigma_work < sigma_allow
Operating temperature
Confirm a junction stays inside the part’s rated range. Temperature uses Kelvin as the underlying dimension:
Ambient: T_amb = 260 :K
Self-heating: delta_T = 40 :K
Junction temperature: T_j = T_amb + delta_T :K
Rated maximum junction: T_max = 400 :K
test: T_j <= T_max
Power budget
Check that available electrical power covers peak demand with headroom:
Supply capability: P_supply = 48 :W
Peak load: P_peak = 35 :W
Minimum design margin: P_margin_min = 5 :W
test: P_supply >= P_peak + P_margin_min
String modes and requirements
Tests can combine numeric checks with string equality if the model uses a parameter to indicate the mode:
Array configuration: config = 'series'
Cell count: n_cells = 12
Cells required for target voltage: n_req = 12
test: config == 'series' and n_cells == n_req
Intervals
In some cases, you may want to calculate using a range of values rather than one single value. This allows you to represent uncertainty. For example, maybe you don’t know what the wind speed will be exactly, but you can estimate that it will be between 5 and 10 kilometers per hour.
To enable this, Oneil provides an interval operator in the form of
<expr> | <expr>.
Note
You may also see references to this as the minmax operator, since it produces the min and the max values.
The interval operator takes two expressions and produces a value representing
the range from the minimum of the expressions to the maximum expressions. These
values can then be retrieved using the min and max functions.
# temperature.on
$ Ambient temperature: t_amb = 300 | 400 :K
$ Ambient temperature range: t_amb_range = max(t_amb) - min(t_amb) :K
oneil eval temperature.on
t_amb = 300 | 400 :K # Ambient temperature
t_amb_range = 100 :K # Ambient temperature range
Arithmetic Operators
The same operators that apply to scalar values apply to interval values: +,
-, *, /, %, and ^.
Because an interval is a range of possible values, not a single number, results can differ from the naive idea of “min with min, max with max.” For example, with subtraction, that naive rule would be wrong:
X: x = 10 | 15
Y: y = 0 | 5
Z: z = x - y
# = (10 | 15) - (0 | 5)
# = 10 - 0 | 15 - 5
# = 10 | 10 # incorrect
Oneil implements subtraction so the range is arithmetically correct:
min(i1) - max(i2) | max(i1) - min(i2).
X: x = 10 | 15
Y: y = 0 | 5
Z: z = x - y
# = (10 | 15) - (0 | 5)
# = 10 - 5 | 15 - 0
# = 5 | 15
For more detail on interval operators, see the interval arithmetic paper review or the implementation in the codebase.
Escaping and relationships
Oneil’s interval arithmetic aims to satisfy the inclusion property: if every interval in an expression is replaced by some scalar inside that interval and the expression is evaluated as scalars, the scalar result lies inside the interval you get by evaluating the expression on intervals.
Bounds can still be wider than necessary. For example, you would expect a - a to be 0 for any a. If a is 0 | 1, interval subtraction yields -1 | 1. That interval still contains the true result 0, but it is looser than
0 | 0. This know as the
dependency problem.
When you need tighter results (for example in geometry, where identities like
a - a = 0 matter), you can leave “pure” interval arithmetic by using
min(i) and max(i) to work on scalars, then build a new interval with |.
For instance, instead of a - a, you can use
min(a) - min(a) | max(a) - max(a).
For common cases, Oneil provides -- and //:
| Operator | Equivalent to |
|---|---|
a -- b | min(a) - min(b) | max(a) - max(b) |
a // b | min(a) / min(b) | max(a) / max(b) |
Comparison Operators
Intervals can be compared with ==, !=, <, <=, >, and >=. The rules
are defined in terms of min and max:
| Operator | Equivalent to | Description |
|---|---|---|
a == b | min(a) == min(b) and max(a) == max(b) | The min and the max are the same |
a != b | min(a) != min(b) or max(a) != max(b) | The min or the max is not the same |
a < b | max(a) < min(b) | The max value of a is less than the min value of b |
a <= b | max(a) <= min(b) | The max value of a is less than or equal to the min value of b |
a > b | min(a) > max(b) | The min value of a is greater than the max value of b |
a >= b | min(a) >= max(b) | The min value of a is greater than or equal to the max value of b |
Notes
Oneil renders parameter equations for review directly from code. This makes it easier to review code with complex equations.
If you’ve ever written a scientific paper, you know that there is often a lot of typeset math and narrative involved in deriving an equation. Showing your work like this helps you and others remember or review the reasons a parameter is expressed the way it is. To help you do this, Oneil supports 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 / class | Description |
|---|---|
oneil.values | Builtin constants (e.g. pi, e) as Python objects |
oneil.functions | Builtin functions (e.g. min, max, sqrt, sin) as callables |
oneil.units | Builtin units (e.g. m, kg, s) as Unit instances, including SI-prefixed variants |
oneil.Interval | Class for closed numeric intervals |
oneil.MeasuredNumber | Class for a number (scalar or interval) with a unit |
oneil.Unit | Class for dimensional units |
oneil.values
Constants matching Oneil’s builtin values. Each is converted to a Python object (float, or other value type as in Oneil).
pi— πe— Euler’s number
Example:
import oneil
# for now, Oneil doesn't support direct imports like the following,
# but it may in the future
#from oneil.values import pi as on_pi, e as on_e
on_pi = oneil.values.pi
on_e = oneil.values.e
print(on_pi, on_e)
oneil.functions
Oneil’s builtin functions as callables. They accept the same conceptual argument types as in Oneil: Python float, oneil.Interval, and oneil.MeasuredNumber. Arguments are converted to Oneil values; on type or unit mismatch they raise TypeError or ValueError.
Supported functions:
min,max— minimum/maximum of numbers (scalars or intervals).sin,cos,tan— trig (radians).asin,acos,atan— inverse trig (result in radians).sqrt— square root.ln,log2,log10— natural, base-2, and base-10 logarithms.floor,ceiling— round down/up to nearest integer.range— with one argument (an interval): max − min; with two arguments: their difference.abs,sign— absolute value and sign.mid— with one argument (an interval): midpoint; with two arguments: midpoint between them.strip— strip units from a measured number, returning the numeric value.mnmx— return both the minimum and maximum of the given values.
Call with positional arguments:
import oneil
# for now, Oneil doesn't support direct imports like the following,
# but it may in the future
#from oneil.functions import sqrt as on_sqrt, min as on_min
on_sqrt = oneil.functions.sqrt
on_min = oneil.functions.min
print(on_sqrt(2.0))
print(on_min(1.0, 2.0, 3.0))
oneil.units
Builtin units as oneil.Unit instances. Names match Oneil’s unit aliases, with two substitutions for valid Python identifiers:
%→percent$→dollar
Units that support SI prefixes (e.g. m, g, s) also have prefixed names (e.g. km, mm, kg, mg, ms).
Examples:
import oneil
from oneil.units import m, kg, seconds, dollar, percent
# prefixed
from oneil.units import km, mm, kg, mg
oneil.Interval
Closed interval of real numbers with a minimum and maximum. Wraps Oneil’s interval type; supports arithmetic and comparison with other Interval instances or with Python scalars (a scalar is treated as a point interval).
oneil.Interval constructor
Interval(min, max)—minandmaxmust not be NaN, andmin≤max; otherwiseValueErroris raised.
oneil.Interval class methods
Interval.empty()— empty interval.Interval.zero()— [0, 0].
oneil.Interval instance methods and properties
min,max— bounds (read-only).is_empty(),is_valid()— emptiness and validity checks.intersection(other)— intersection with anotherInterval.tightest_enclosing_interval(other)— smallest interval containing both.contains(other)— whether this interval contains the other.
Arithmetic and comparison: +, -, *, /, %, **, unary +/-, and ==, !=, <, <=, >, >=. The other operand may be an Interval or a scalar (float).
Math methods (return new Interval): sqrt, ln, log10, log2, abs, sign, sin, cos, tan, asin, acos, atan, floor, ceiling, pow(exponent) (exponent is an Interval).
Specialized (interval) operations:
escaped_sub(other)— subtract using (min−min, max−max).escaped_div(other)— divide using (min/min, max/max).
oneil.MeasuredNumber
A number (scalar or interval) with a unit. Wraps Oneil’s measured number type; arithmetic and comparison enforce dimensional consistency where required.
oneil.MeasuredNumber constructor
MeasuredNumber(value, unit)—valueis afloat, anInterval, or aMeasuredNumber;unitis aUnit. Builds a measured number from that value and unit.
oneil.MeasuredNumber instance methods
unit()— returns theUnitof this measured number.into_number_and_unit()— returns a tuple(number, unit)wherenumberis the numeric part (float orInterval) in this object’s unit.into_number_using_unit(unit)— converts to a number (float orInterval) in the givenUnit; raises if dimensions don’t match.into_unitless_number()— same as converting to a dimensionless unit; raises if not dimensionless.with_unit(unit)— returns a copy with the given unit; raises if not dimensionally equivalent.
Arithmetic: +, -, *, /, %, ** with other MeasuredNumber or, when the measured number is effectively unitless, with plain numbers. Unit mismatches raise ValueError.
Comparison: ==, !=, <, <=, >, >= (with same conversion rules as arithmetic).
Math (return MeasuredNumber): sqrt, ln, log10, log2, abs, floor, ceiling.
Other:
min(),max()— minimum/maximum as measured numbers.min_max(other)— tightest enclosing measured number of this andother.escaped_sub(other),escaped_div(other)— escaped subtraction and division (units must match).
oneil.Unit
Represents a dimensional unit (dimensions, magnitude, optional decibel flag, and display info).
oneil.Unit constructor
Unit(*, dimensions=None, magnitude=None, is_db=None, display_unit)dimensions— optional dict mapping dimension keys to exponents (e.g.{"m": 1, "s": -1}). Valid keys:"kg","m","s","K","A","b","$","mol","cd".magnitude— optional scale (default 1.0).is_db— optional decibel flag (defaultFalse).display_unit— required string used as the display name (single unit, exponent 1).
oneil.Unit class methods
Unit.one()— dimensionless unit 1.
oneil.Unit properties and methods
magnitude,is_db,display_string— magnitude, decibel flag, and display string.get_dimensions()— dict of dimension key → exponent.is_dimensionless()— whether the unit is dimensionless.dimensionally_eq(other)— same dimensions as anotherUnit.dimensions_match(dimensions)— dimensions match the given dict.numerically_eq(other)— same dimensions, magnitude, andis_db.
Arithmetic: *, /, ** (exponent as float) with other Unit instances.
with_is_db_as(is_db)— copy with decibel flag set.mul_magnitude(factor)— copy with magnitude multiplied.pow(exponent)— unit raised to a power.
Value conversion (Python ↔ Oneil)
From Python into Oneil’s value type:
bool→ booleanstr→ stringfloat→ scalar numberoneil.Interval→ interval numberoneil.MeasuredNumber→ measured number
From Oneil to Python:
- Boolean →
bool - String →
str - Scalar number →
float - Interval number →
oneil.Interval - Measured number →
oneil.MeasuredNumber
The builtin functions in oneil.functions use this mapping for their arguments and return values. Passing an unsupported type raises TypeError with a message that includes the received type.
Appendix B: Using AI
Oneil can be used effectively with AI to model and design systems. The following is an example ruleset for Oneil.
---
description: Senior systems engineer with experience in Oneil
globs: *.on, *.one
alwaysApply: true
---
# Oneil Development Rules
You are an experienced systems engineer. 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`.