# Apsi documentation (complete) # Getting started This guide takes you from installation to a first program, a first trained model and a first library in about ten minutes. ## 1. Install Apsi 2.4.4 runs on **Windows 10 or newer** and **Linux** (x86-64) and needs **Python 3.12 or newer** as its host runtime. The installers check for Python and offer to install it. - Windows: download `ApsiSetup-2.4.4-x64.exe` from [apsi.dev/download](https://apsi.dev/download) and run it. Details: [Install on Windows](/docs/2.4.4/install). - Linux: download `ApsiSetup-2.4.4-linux.sh` from [apsi.dev/download](https://apsi.dev/download) and run `sh ApsiSetup-2.4.4-linux.sh`, or install the `.deb` with `sudo apt install ./apsi_2.4.4_all.deb`. Details: [Install on Linux](/docs/2.4.4/install-linux). Check the installation: ```bash apsi version apsi doctor # checks Python, the native compiler and your hardware ``` ## 2. Your first program ```bash apsi new hello cd hello apsi run ``` `apsi new` creates a project with `apsi.toml`, `src/main.apsi`, tests and editor settings. Open `src/main.apsi`: ```apsi fn greet(who: Str) -> Str = f"Hello, {who}!" let g = 9.81[m/s^2] let t = 2s print(greet("Apsi")) print(f"a stone falls {0.5 * g * t ** 2} in {t}") ``` Physical units are part of the language. Change `t` to `2kg` and run again: the checker reports the unit error before anything runs. ## 3. Make it fast Mark a function `@native` and it is compiled to machine code for your processor the first time it is called. The machine code is cached on disk, so later runs start immediately. ```apsi @native fn mandel(n: Int, maxit: Int) -> Int: var count = 0 for y in 0..n: for x in 0..n: let cr = 2.0 * x / n - 1.5 let ci = 2.0 * y / n - 1.0 var zr = 0.0 var zi = 0.0 var it = 0 while it < maxit and zr * zr + zi * zi <= 4.0: let t = zr * zr - zi * zi + cr zi = 2.0 * zr * zi + ci zr = t it += 1 if it == maxit: count += 1 return count print(mandel(1000, 50)) ``` See the [native compiler guide](/docs/2.4.4/native) for what can be compiled and how to use all cores with `parallel for`. ## 4. Train a model ```apsi use torch let model = torch.Sequential(torch.Linear(2, 32), torch.ReLU(), torch.Linear(32, 1)) var params = model.init(0) let opt = torch.AdamW(lr=1e-2) let x = randn(256, 2, seed=1) let y = sum(x * x, axis=1, keepdims=true) for step in 0..500: fn loss_of(p) = torch.mse_loss(model(p, x), y) let loss, grads = value_and_grad(loss_of)(params) params = opt.step(params, grads) if step % 100 == 0: print(f"step {step}: loss {loss}") ``` Already have PyTorch code? Convert it: ```bash apsi convert train.py -o train.apsi apsi run train.apsi ``` See [From Python and PyTorch](/docs/2.4.4/python). ## 5. Use and publish libraries ```bash apsi install physics-units # newest version from the registry ``` ```apsi use physics_units ``` To publish your own, create a library project, build it into `dist/` and upload it with an API token: ```bash apsi new my-lib --template lib cd my-lib apsi pack apsi publish --token apsi_… ``` See [Libraries](/docs/2.4.4/libraries). ## 6. Where to go next - [What's new in 2.4.4](/docs/2.4.4/whats-new) — exact Python conversion, real time, memory, tokens, tables, databases. - [From Python](/docs/2.4.4/python) — `apsi convert` an existing program and run it unchanged. - [Language reference](/docs/2.4.4/language) — syntax, types, units, modules. - [AI and machine learning](/docs/2.4.4/ai) and [Reinforcement learning](/docs/2.4.4/rl). - [Science, physics and medicine](/docs/2.4.4/science). - [Troubleshooting](/docs/2.4.4/troubleshooting) if something does not work. # You already know Apsi — three ways to write it Apsi is designed so that **you do not have to learn a new language before you are productive**. If you can read Python, you can read Apsi; if you can write Python, there are three ways to get working Apsi code today: | Way | Best when | What you do | |---|---|---| | **1. Write it like Python** | you are starting something new | write the Python you know, with four small changes (below) | | **2. Let an AI write it** | you want speed | give an AI assistant the short brief below and ask for Apsi | | **3. Write Python and convert** | you already have Python code | `apsi convert script.py` — then run it | All three end in the same place: a program that is **checked before it runs** (units, types, shapes, names) and runs on Apsi's fast engines. ## 1. Write it like Python Almost everything is the same: indentation, `if/elif/else`, `for`/`while`, `break`/`continue`, lists, dicts, tuples, slicing, comprehensions, f-strings, `try/except/finally`, `raise`, `yield`, lambdas, `*args/**kwargs`, string and list methods. The differences that matter: | Python | Apsi | Why it is better | |---|---|---| | `x = 1` | `let x = 1` (never changes) or `var x = 1` (changes) | accidental reassignment is caught before running | | `def f(a, b=2):` | `fn f(a, b = 2):` — or one line: `fn f(a) = a * 2` | short functions read like formulas | | `class P:` + `__init__` | `struct P:` with `name: Type` fields | fields are declared once; construction is checked | | `lambda x: x * 2` | `fn(x) => x * 2` | | | `range(n)` | `0..n` (or `range(n)`, both work) | ranges also work with units: `0s..10s` | | `None True False` | `none true false` | | | `import numpy as np` | built in: `tensor`, `zeros`, `@`, `sum`, `linspace` … | no imports for maths, tensors, AI, ODEs | And one thing Python does not have at all — **physical units**: `let v = 3[m/s]`, `let t = 2s`, `v * t` is `6 m`; `v + t` is reported as an error before the program runs. ```apsi # Python habits work: classes, errors, generators, comprehensions struct Account: owner: Str balance: Float = 0.0 fn withdraw(self, amount): if amount > self.balance: raise ValueError(f"only {self.balance} left") self.balance -= amount let acc = Account("ana", 100.0) try: acc.withdraw(250.0) except ValueError as e: print("refused:", e.message) ``` ## 2. Let an AI write it Modern assistants (Claude, ChatGPT, Gemini, Copilot, Cursor …) write good Apsi when they get this brief first. Copy it, then describe your program: ```text Write the program in Apsi (https://apsi.dev), a Python-like language for AI and physics. Rules: indentation blocks like Python; `let` for constants and `var` for variables; `fn name(a, b = 2):` or one-line `fn f(x) = expr`; lambdas `fn(x) => expr`; `struct Name:` with `field: Type = default` and methods taking `self`; ranges `0..n`; `none/true/false`; try/except/finally, raise, yield, *args/**kwargs as in Python. Physical quantities carry units: 9.81[m/s^2], 2kg, 3s; annotate with Length, Mass, Time, Velocity, Energy … Tensors are built in: tensor, zeros, randn, @, sum, mean; `use nn`, `use rl`, `use torch` for AI. Built-ins and modules: see https://apsi.dev/docs/2.4.4/reference . Prefer built-ins over Python libraries. ``` The site publishes [`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) — the whole documentation in plain text — so assistants that browse can read everything themselves. After the AI answers, run `apsi check program.apsi`: the checker points at the exact line of anything the AI got wrong, and you (or the AI) fix it in seconds. ## 3. Write Python and convert it ```bash apsi convert script.py # writes script.apsi next to it and checks it apsi convert src/ -o apsi_src/ # a whole folder apsi convert train.py --run # convert, check and run ``` The converter understands Python's meaning, not only its spelling: classes become structs with their methods, `__add__`/`__eq__`/`__len__`/`__str__` become the struct methods that Apsi's operators call, exceptions keep their kinds (`except KeyError` still catches a missing key), generators stay lazy, `for … else` keeps its rule, numpy becomes Apsi tensors and **whole PyTorch training programs become native Apsi that runs without Python or PyTorch** — matching PyTorch's numbers to 1e-9. Pure standard modules (`collections`, `heapq`, `itertools`, `re`, `datetime` …) are used as they are. Every converted test program in `tests/pycompat/` prints exactly what Python prints. Details and the complete mapping: [From Python and PyTorch](/docs/2.4.4/python). ## Freedom, not a cage Apsi gives you safe defaults — units, checks, a sandbox — and then gets out of the way: - **Any Python library** still works: `let pd = pyimport("pandas")` (run with `--allow-python`). - **C and C++** libraries: `cload("libfoo.so")` and call functions directly ([interop](/docs/2.4.4/interop)). - **Your own operators, iterators, printing** on your types (struct methods `add`, `iter`, `to_str` …). - **Your own libraries** for everyone: `apsi pack && apsi publish` ([libraries](/docs/2.4.4/libraries)). - **Servers, tools, data pipelines, simulations, models** — the same language for all of them. # Installing Apsi on Windows and Linux ## Supported systems | | Target | |---|---| | Operating system | **Windows 10 / 11** (build 10240 or newer), **Linux** kernel 4.15+ (Ubuntu 18.04+, Debian 10+, Fedora, Arch, …) | | CPU | x86-64-v3 (AVX2 + FMA): Intel Core i5-7500 / i7-6700 or newer, any AMD Ryzen, or ARM64 | | GPU (optional) | NVIDIA compute capability 7.5+ (GTX 1660 / RTX 20xx or newer) + CuPy | | Python | 3.12 or newer (tested with 3.12, 3.13 and 3.14; the Windows installer and the portable Linux build bring their own 3.12) | After installing, run `apsi doctor` to check your machine against these targets. --- ## The easy way: the ready-made installers (recommended) | System | File | How | |---|---|---| | Windows 10/11 (64-bit) | `ApsiSetup-2.4.4-x64.exe` | double-click (contains Python 3.12 + numpy; no administrator rights needed) | | Windows, portable | `apsi-2.4.4-windows-x64.zip` | unzip and run `apsi.cmd` (works from a USB stick) | | Ubuntu / Debian / Mint | `apsi_2.4.4_all.deb` | `sudo apt install ./apsi_2.4.4_all.deb` | | Any system with Python 3.12+ | `apsi-2.4.4-py3-none-any.whl` | `pip install apsi-2.4.4-py3-none-any.whl` | **Download:** the Windows installer, the Linux installer (`ApsiSetup-2.4.4-linux.sh`) and the `.deb` are on [apsi.dev/download](https://apsi.dev/download), each with its SHA-256. When you build Apsi yourself ([BUILD.md](/docs/2.4.4/build)), all of these files are in the `dist/` folder. ### What the Windows setup wizard does 1. Shows *Preparing Apsi Setup…* immediately while it unpacks, then keeps that window on top of other windows until the wizard has finished loading, closes it and opens the wizard in its place. 2. Lets you choose **Express** (recommended defaults) or **Custom** (folder, components, optional AI packages, security profile). 3. Installs to `%LOCALAPPDATA%\Programs\Apsi` — per user, no administrator rights. 4. Adds `apsi` to your user PATH. Windows that were already open keep the old PATH: open a new terminal (quit VS Code completely) or see [`TROUBLESHOOTING.md`](/docs/2.4.4/troubleshooting) for a command that fixes the current window. 5. Registers `.apsi` files: the Apsi icon, **Run with Apsi** (double-click), **Edit** (VS Code when installed, otherwise Notepad), **Test with Apsi**, and **New → Apsi source file** in Explorer's right-click menu. 6. Creates Start-menu entries: Apsi REPL, Examples, Documentation, Uninstall. 7. Adds Apsi to the code editors it finds — VS Code, Cursor, Windsurf, VSCodium, VS Code Insiders, Trae, Sublime Text, Neovim, Vim and Notepad++ — and lists them on the *System check* page (see [`EDITORS.md`](/docs/2.4.4/editors)). 8. Registers in *Settings → Apps* so it can be removed normally. Every change is recorded and undone on uninstall; a failed install rolls back completely. Unattended install: `ApsiSetup-2.4.4-x64.exe --silent [--dir PATH] [--profile strict] [--no-editors] [--with-torch]`. Windows older than 10 and 32-bit Windows are not supported. ### What the .deb package does - installs `python3-numpy` from apt automatically - creates the `/usr/bin/apsi` command - puts the examples and documentation in `/usr/share/apsi/examples` and `/usr/share/apsi/docs` --- ## Windows 10 / 11 from source ### 1. Install Python (if you do not have it) ```powershell winget install Python.Python.3.12 ``` Or download it from python.org and **tick "Add python.exe to PATH"** during setup. ### 2. Install Apsi Double-click `install\install.bat`, or run in PowerShell: ```powershell powershell -ExecutionPolicy Bypass -File install\install.ps1 ``` The script: - finds Python 3.12+ (`py -3` first, then `python`) - creates a separate virtual environment in `%LOCALAPPDATA%\Apsi\venv`, so other Pythons on the machine are untouched - creates the `apsi` command (`%LOCALAPPDATA%\Apsi\bin\apsi.cmd`) and adds it to your user PATH, without administrator rights - sets the console to UTF-8 so symbols such as ✓ ± ² display correctly Open a new terminal and try: ```powershell apsi version apsi run examples\01_hello.apsi apsi doctor ``` Uninstall: `powershell -ExecutionPolicy Bypass -File install\install.ps1 -Uninstall` ### Without installing ```bat pip install numpy apsi.bat run examples\01_hello.apsi ``` ### Windows tips - **Windows Terminal** (included with Windows 11) shows Unicode symbols such as ✓ ± ² best - `.apsi` files saved by Notepad (UTF-8 with BOM, CRLF line endings) work as they are - the REPL understands Windows paths directly: `cd C:\Users\me\project`, `run app.apsi`, `ls` - compiling C for the FFI: `examples\native\build.bat` works with MSVC (`cl`) and MinGW-w64 (`gcc`) - `cload("native/vecmath")` finds `vecmath.dll` on Windows and `libvecmath.so` on Linux - shell completion: `apsi completion powershell | Out-String | Invoke-Expression` (add it to your `$PROFILE`) --- ## Linux from source ### 1. Install Python and venv ```bash # Ubuntu / Debian sudo apt install python3 python3-venv # Fedora sudo dnf install python3 # Arch sudo pacman -S python ``` ### 2. Install Apsi ```bash sh install/install.sh ``` - installs into `~/.apsi/venv` and creates `~/.local/bin/apsi` without sudo - if `~/.local/bin` is not on your PATH yet, the script prints the line to add to `~/.bashrc` Uninstall: `sh install/install.sh --uninstall` ### Without installing ```bash pip install numpy ./apsi.sh run examples/01_hello.apsi ``` ### Executable scripts ```bash #!/usr/bin/env apsi ``` Put this line at the top of a file and `chmod +x` it to run `.apsi` files like any other program (a `#` line is a comment). --- ## Optional extras | You want | Install | Used with | |---|---|---| | NVIDIA GPU | `apsi pkg install gpu` | `use gpu` → `to_gpu(x)` | | PyTorch (picks the CUDA build that matches your NVIDIA driver, or CPU) | `apsi pkg install torch-auto` | `fit(…, backend="torch")` on the GPU, `torch_module`, `to_torch` (`--allow-python`) — see [`BACKENDS.md`](/docs/2.4.4/backends) | | PyTorch + ONNX Runtime + Transformers | `apsi pkg install ai` (or `ai-gpu`) | `use models` (`--allow-python`) | | Editor support | `apsi editors install` | VS Code, Cursor, Windsurf, VSCodium, Sublime Text, Vim/Neovim, Notepad++ — see [`EDITORS.md`](/docs/2.4.4/editors) | | Tab completion and history in the REPL on Windows | `pip install pyreadline3` (included in the setup wizard) | `apsi` | ## Problems after installing `apsi` not recognized, no colours in VS Code, `needs permission 'python'`: see [`TROUBLESHOOTING.md`](/docs/2.4.4/troubleshooting). ## Checking the installation ```bash apsi doctor # hardware and operating system apsi verify # runtime files match the release manifest apsi test examples tests/apsi --allow-all # the whole language test suite python -m unittest discover tests # the toolchain test suite ``` If you keep the code on GitHub, `.github/workflows/ci.yml` runs every test automatically on Windows Server 2022/2025 and Ubuntu 22.04/24.04 with Python 3.12, 3.13 and 3.14. # Apsi 2.4.4 for Linux Works on every x86-64 distribution with glibc 2.17 or newer, such as Ubuntu 18.04+, Debian 10+, Fedora, Arch, openSUSE, Mint and Pop!_OS (the shell installer also on ARM, and in WSL on Windows). **Download:** `ApsiSetup-2.4.4-linux.sh` and `apsi_2.4.4_all.deb` are on [apsi.dev/download](https://apsi.dev/download). The other files below are made when you build Apsi yourself ([BUILD.md](/docs/2.4.4/build)). | File | Best for | Bundled Python | |---|---|---| | `ApsiSetup-2.4.4-linux.sh` | **simplest** — one-file installer for every distribution, no root, works offline | uses the system python3 (installs numpy privately if missing) | | `ApsiSetup-2.4.4-linux-x86_64.AppImage` | **recommended** — graphical setup (Express/Custom, security, Apsi Guard) | ✓ 3.12 + numpy | | `apsi-2.4.4-linux-x86_64.tar.gz` | portable — unpack and use, no install, no root | ✓ 3.12 + numpy | | `apsi_2.4.4_all.deb` | Ubuntu/Debian system install with apt | uses the system python3 | | `apsi-2.4.4-py3-none-any.whl` | an existing Python 3.12+ or a venv | – | | `apsi-language-2.4.4.vsix` | VS Code extension | – | ## 0) Shell installer (`ApsiSetup-2.4.4-linux.sh`) ```bash sh ApsiSetup-2.4.4-linux.sh # install for the current user sh ApsiSetup-2.4.4-linux.sh --no-editors # without adding Apsi to code editors sh ApsiSetup-2.4.4-linux.sh --prefix ~/apsi # somewhere else (default: ~/.local/share/apsi) ~/.local/share/apsi/uninstall.sh # remove everything again ``` It needs `python3` 3.12 or newer (Ubuntu 24.04+, Debian 13+, Fedora 39+, Arch, openSUSE Tumbleweed; on older systems install one with `uv python install 3.12` or use the portable build, which brings its own Python). It installs: - Apsi with its documentation and examples into `~/.local/share/apsi` - the commands `apsi`, `apsi-repl` and `apsi-run` into `~/.local/bin` (it tells you if that folder is not on your `PATH` yet) - the `.apsi` file type with its icon and *Run with Apsi*, *Apsi REPL* in the application menu, and *Apsi file* in *New Document* when `~/Templates` exists - Apsi support in the code editors it finds (VS Code, Cursor, Windsurf, VSCodium, Sublime Text, Vim/Neovim) numpy comes from the system when it is installed (`python3-numpy`); otherwise the installer creates a private Python environment and installs numpy into it with pip. Running it again updates Apsi in place. In WSL (Linux on Windows) it works the same way. ## 1) AppImage setup (graphical) ```bash chmod +x ApsiSetup-2.4.4-linux-x86_64.AppImage ./ApsiSetup-2.4.4-linux-x86_64.AppImage ``` If you get a FUSE error (Ubuntu 22.04 and newer), do one of these: ```bash sudo apt install libfuse2 # Ubuntu 24.04+: sudo apt install libfuse2t64 ./ApsiSetup-2.4.4-linux-x86_64.AppImage --appimage-extract-and-run # or run without FUSE ``` Install without a window (servers, SSH): ```bash ./ApsiSetup-2.4.4-linux-x86_64.AppImage --appimage-extract-and-run --silent --profile strict ``` Apsi is installed in `~/.local/share/apsi` and the `apsi` command in `~/.local/bin`. The setup also registers the `text/x-apsi` file type (icon and *Open with Apsi*), adds *Apsi REPL* to the application menu and, when `~/Templates` exists, an *Apsi file* entry to *New Document*. Uninstall with `~/.local/share/apsi/uninstall.sh`, or open the AppImage again and choose Uninstall. ## 2) Portable (tar.gz) ```bash tar xzf apsi-2.4.4-linux-x86_64.tar.gz cd apsi-2.4.4-linux-x86_64 ./bin/apsi version ./bin/apsi run examples/01_hello.apsi ./install.sh # (optional) link the apsi command into ~/.local/bin ``` ## 3) .deb package ```bash sudo apt install ./apsi_2.4.4_all.deb apsi version ``` Examples and documentation are in `/usr/share/apsi/`. Remove with `sudo apt remove apsi`. ## 4) pip / venv ```bash python3 -m venv ~/apsi-env && . ~/apsi-env/bin/activate pip install apsi-2.4.4-py3-none-any.whl ``` ## After installing ```bash apsi doctor # CPU, GPU and RAM against Apsi's targets apsi pkg install ai # PyTorch + ONNX + Transformers apsi pkg install ai-gpu # instead of the line above, with an NVIDIA card (PyTorch + CUDA 12.6) apsi scan ~/Downloads/some-project # malware scan (add --av to also run ClamAV) apsi config --profile strict # default security level eval "$(apsi completion bash)" # tab completion (add to ~/.bashrc) code --install-extension apsi-language-2.4.4.vsix ``` ## Problems after installing `apsi: command not found`, no colours in VS Code, `needs permission 'python'`: see [`TROUBLESHOOTING.md`](/docs/2.4.4/troubleshooting). ## Checking downloads ```bash sha256sum -c SHA256SUMS.txt ``` # Troubleshooting This page covers the problems people most often hit right after installing Apsi, with the exact commands to fix them. - [`apsi` is not recognized / `command not found`](#apsi-is-not-recognized--command-not-found) - [VS Code shows no syntax colours, or Ctrl+F5 does nothing](#vs-code-shows-no-syntax-colours-or-ctrlf5-does-nothing) - [`needs permission 'python'`](#needs-permission-python) - [A function prints "runs on the bytecode engine"](#a-function-prints-runs-on-the-bytecode-engine) - [`Apsi 2.4.4 needs Python 3.12 or newer`](#apsi-244-needs-python-312-or-newer) - [A converted PyTorch program reports notes](#a-converted-pytorch-program-reports-notes) --- ## `apsi` is not recognized / `command not found` **Typical messages** ```text apsi : The term 'apsi' is not recognized as the name of a cmdlet, function, script file, or operable program. (PowerShell) 'apsi' is not recognized as an internal or external command, operable program or batch file. (Command Prompt) bash: apsi: command not found (Linux) ``` **Why it happens.** The installer adds the folder that contains the `apsi` command to your `PATH`. A program reads `PATH` once, when it starts. Every terminal window that was already open before the installation finished still has the old `PATH`, so it cannot find `apsi`, even though the installation succeeded. **The fix.** Close the terminal and open a new one. - **VS Code:** its built-in terminal inherits `PATH` from VS Code itself, so opening a new terminal tab is not enough. Quit VS Code completely (**File → Exit**, so that every VS Code window is closed) and start it again. The same applies to Cursor, Windsurf, VSCodium and other editors with a built-in terminal. - **Windows Terminal:** close the whole Windows Terminal application, not just one tab. ### Use Apsi in the current window without restarting it These commands update only the window you type them in. New windows pick up the change by themselves. **PowerShell** (reloads `PATH` exactly as Windows now has it saved): ```powershell $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') apsi version ``` **Command Prompt** (`cmd.exe` cannot reload `PATH`, so add the Apsi folder for this session): ```bat set "PATH=%LOCALAPPDATA%\Programs\Apsi\bin;%PATH%" apsi version ``` If you installed from source with `setup.ps1`, the folder is `%LOCALAPPDATA%\Apsi\bin` instead. **Linux** (shell installer, AppImage setup and `pip --user` installs put the command in `~/.local/bin`): ```bash export PATH="$HOME/.local/bin:$PATH" hash -r apsi version ``` To make this permanent on Linux, add the `export` line to `~/.bashrc` (or `~/.zshrc`). Many distributions only add `~/.local/bin` to `PATH` at login when the folder already existed, so logging out and back in once also fixes it. The `.deb` package installs `/usr/bin/apsi`, which is always on `PATH`. ### Check whether Apsi is on PATH **PowerShell** ```powershell Get-Command apsi # shows the full path of the apsi command, if found [Environment]::GetEnvironmentVariable('Path', 'User') -split ';' | Select-String -SimpleMatch 'Apsi' Test-Path "$env:LOCALAPPDATA\Programs\Apsi\bin\apsi.cmd" # True when the command file exists ``` **Command Prompt** ```bat where apsi echo %PATH% ``` **Linux** ```bash command -v apsi # prints the path of the apsi command, if found echo "$PATH" | tr ':' '\n' | grep -x "$HOME/.local/bin" # prints the folder when it is on PATH ls -l ~/.local/bin/apsi # the command file itself ``` How to read the result: | Saved PATH contains the Apsi folder | Command file exists | What to do | |---|---|---| | yes | yes | Only the window is out of date: open a new window, or run the command above for your shell. | | no | yes | The PATH entry was not written (for example, *Add apsi to PATH* was unticked in Custom setup). Run the setup again, or add the folder to your user PATH under **Settings → System → About → Advanced system settings → Environment Variables**. | | no | no | Apsi is not installed for this user. Run the installer again. | You can always start Apsi without `PATH` by using the full path, for example `& "$env:LOCALAPPDATA\Programs\Apsi\bin\apsi.cmd" version` in PowerShell or `~/.local/bin/apsi version` on Linux. --- ## VS Code shows no syntax colours, or Ctrl+F5 does nothing **Why it happens.** VS Code loads extensions, and reads `PATH`, when it starts. If it was open while Apsi was being installed, it neither knows about the Apsi extension nor can it find the `apsi` command that **Apsi: Run File** (`Ctrl+F5`) uses. **The fix.** 1. Quit VS Code completely (**File → Exit**) and start it again. 2. If `.apsi` files are still shown as plain text, install the extension again from a terminal: ```bash apsi editors install vscode ``` Then reload the window: press `Ctrl+Shift+P`, type **Developer: Reload Window**, and press Enter. **Check that it worked.** - The language indicator in the bottom-right corner of the status bar shows **Apsi** when an `.apsi` file is open. If it shows **Plain Text**, click it and choose **Apsi**. - `code --list-extensions` lists `apsi.apsi-language`. - `apsi editors list` shows which editors Apsi found and whether Apsi support is installed in each. If `Ctrl+F5` still does nothing while colours work, the extension cannot find the `apsi` command: follow [the PATH section above](#apsi-is-not-recognized--command-not-found), then restart VS Code once more. Another extension can also take over `Ctrl+F5`; the same command is available as **Apsi: Run File** from the Command Palette and as `Ctrl+Alt+N`. --- ## `needs permission 'python'` **Typical message** ```text app.apsi:3:1: PermissionError: pyimport('pandas') needs permission 'python'; run with --allow-python or --allow-all ``` **Why it happens.** Apsi programs cannot use Python libraries (`pyimport`, `use models`, the PyTorch backend, `to_torch`, ...) unless you allow it, because Python code can do things that Apsi's permission system cannot check. The error means your program uses Python and the permission was not given. **The fix.** Add `--allow-python` to the command: ```bash apsi run app.apsi --allow-python apsi test --allow-python ``` `--allow-python` allows ordinary libraries such as numpy, pandas, torch and transformers. `--allow-python=all` also allows system modules such as `subprocess` and `os`, which can bypass Apsi's file and network permissions; use it only for code you trust. Avoid `--allow-all` unless you need every permission. To avoid typing the flag every time in a project, list the permission in the project's `apsi.toml`: ```toml [run] allow = ["python"] ``` Executables made with `apsi build` take the same flag when they are built: `apsi build app.apsi --allow-python`. --- ## A function prints "runs on the bytecode engine" ```text apsi: note: render() runs on the bytecode engine: lists (line 12) ``` This is a note, not an error: the program keeps running with correct results. A function marked `@native` uses something the native compiler does not handle yet (the note names it and gives the line), so that function runs on the bytecode engine instead of as machine code. Rewrite that line using the constructs listed in [`NATIVE.md`](/docs/2.4.4/native) to get machine-code speed, use `@native("strict")` to turn the note into an error, or `@native("quiet")` (or `APSI_NATIVE_QUIET=1`) to hide it. `apsi native explain app.apsi` lists every such function. ## `Apsi 2.4.4 needs Python 3.12 or newer` Apsi runs on Python 3.12 and every newer release. This message means the `apsi` command, `pip` or your editor started an older Python. - **Windows:** the installer (`ApsiSetup-2.4.4-x64.exe`) brings its own Python 3.12, so nothing else is needed. To use `pip install` instead, install a current Python first: ```powershell winget install Python.Python.3.13 py -3.13 -m pip install apsi-2.4.4-py3-none-any.whl ``` - **Linux:** Ubuntu 24.04+, Debian 13+, Fedora 39+, Arch and openSUSE Tumbleweed ship Python 3.12 or newer. On older systems, use the portable build (`apsi-2.4.4-linux-x86_64.tar.gz`, which brings its own Python), or install a newer Python next to the system one: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh uv python install 3.12 uv venv --python 3.12 ~/.apsi-venv ~/.apsi-venv/bin/pip install apsi-2.4.4-py3-none-any.whl ``` - **Check which Python is used:** ```bash python3 --version # Linux py -0p # Windows: every installed Python ``` ## A converted PyTorch program reports notes `apsi convert` turns PyTorch programs into native Apsi and lists, with line numbers, the few things it cannot translate exactly (for example learning-rate schedulers or `nn.MultiheadAttention`). The rest of the program is complete and runs without Python. Review the listed lines; [`PYTHON.md`](/docs/2.4.4/python#pytorch-programs--native-apsi) explains each one. # Versions: what each one can do | | **2.4.4** (current) | **2.0.0** | **1.10.0** | |---|---|---|---| | Released | 10 November 2026 | November 2026 | 8 November 2026 | | Engines | bytecode engine + native compiler + Python-exact mode (`use pycompat`) | bytecode engine **+ native compiler** (`@native`, LLVM machine code), `parallel for`, NVIDIA GPU kernels | bytecode engine | | Speed on the [benchmarks](/benchmarks) | as 2.0.0; HTTP server about 3× faster, cold compile 32 % faster | next to C, C++, Rust; ahead of Go | about 500× slower than 2.0.0 (geometric mean; 12× to 2,400× per program) | | Errors | as 2.0.0, plus `except*` and exception groups | `try / except / else / finally`, `raise`, Python kind names | `error("…")` stops the program | | Generators, `*args/**kwargs`, operator methods on structs | yes, plus real classes, `with`, `match`, `async` | yes | no | | Python conversion | **byte-identical behaviour** for any Python 3.13 program (verified on 34 standard-library modules) | keeps exceptions, generators, classes with operators, `for … else`, pure stdlib modules | core language and numpy; exceptions become comments | | PyTorch | as 2.0.0 | exact conversion of whole training programs, native layers/optimizers/schedulers | `use nn` and `use layers` (PyTorch backend), conversion of models | | Reinforcement learning, science modules | as 2.0.0 | `use rl`, special functions, statistics, DSP, PDE, quantum, mechanics, medical, lab | AI and physics basics | | Large models | as 2.0.0 | `apsi plan`, model files (GGUF, safetensors, `.apsim`), sharding | — | | Libraries (`apsi install / pack / publish / yank / unpublish`) | as 2.0.0 | yes | yes | | Website commands (`apsi release …`) | as 2.0.0 | yes | yes | | Standard modules | 44 modules, 1151 members, 315 built-ins | 34 modules, 833 members, 253 built-ins | 22 modules, 360 members, 201 built-ins | | Real time, video | `use rt`: 1–2 kHz loops, video 640×480 → 4K at any size | — | — | | RAM / SSD / GPU management | `use memory` | — | — | | Fewer AI tokens | `use tokens` (exactly reversible) | — | — | | Tables, databases | `use dataframe`, `use db` (PostgreSQL, MySQL, MongoDB, Redis, SQLite) | — | — | | WebSocket, `.env`, debugger | yes | — | — | | Documentation | [2.4.4 docs](/docs/2.4.4) | [2.0.0 docs](/docs/2.0.0) | [1.10.0 docs](/docs/1.10.0) | ## Which one should I use? **2.4.4** for everything new. Programs written for 2.0.0 and 1.10.0 run on 2.4.4 unchanged — each version only adds. Keep **2.0.0** or **1.10.0** only if a machine cannot install 2.0.0's native compiler dependency (`llvmlite`); even then 2.0.0 works without it, on the bytecode engine, so the reasons are rare. ## Moving from 2.0.0 to 2.4.4 Install 2.4.4 (it replaces 2.0.0) and run `apsi test`. Python files you converted with 2.0.0 can be converted again (`apsi convert file.py`) to get Python's exact behaviour. ## Moving from 1.10.0 to 2.0.0 1. Install 2.0.0 (it replaces 1.10.0). 2. Run your tests: `apsi test`. 3. Add `@native` to the functions that do the heavy loops — see [maximum performance](/docs/2.0.0/best-performance). 4. If you used `error()` for things you would like to recover from, switch to `raise` and `try / except`. # What's new in Apsi 2.4.4 This page walks through everything 2.4.4 adds, with a working example for each part, the numbers we measured, and the limits you should know about. Every example runs as shown with Apsi 2.4.4. The full list of changes is in the [changelog](https://github.com/ATECTechnologies/apsi/blob/main/CHANGELOG.md); each part links to its complete guide. | Part | In one line | Guide | |---|---|---| | Python, exactly | any Python 3.13 program converts and behaves byte for byte the same | [From Python](/docs/2.4.4/python) | | Real time | 1–2 kHz control loops and video from 640×480 to 4K | [Real time](/docs/2.4.4/realtime) | | RAM, SSD, GPU | arrays that move to the SSD instead of running out of memory; container limits and memory pressure | [Memory](/docs/2.4.4/memory) | | Fewer AI tokens | exactly reversible packing of what you send to language models | [Tokens](/docs/2.4.4/tokens) | | Hugging Face | download and run models (BERT family, GPT-2, Llama, Mistral, Qwen, Gemma) without Python packages | [Hub](/docs/2.4.4/hub) | | Tables | a columnar engine, up to 21× faster than pandas (filters; joins 3.7×, group-bys 4–6×) | [Tables](/docs/2.4.4/dataframe) | | Databases | PostgreSQL, MySQL / MariaDB, MongoDB, Redis, SQLite without drivers | [Databases](/docs/2.4.4/databases) | | Web | an HTTP server 2–3× faster than 2.0.0 per process, one process per CPU with `processes="auto"`, uvloop, WebSocket | [Servers](/docs/2.4.4/server) | | Configuration | `.env` files, secrets, Python virtual environments | [Configuration](/docs/2.4.4/env) | | Debugger | breakpoints and stepping in VS Code or the terminal | [Debugging](/docs/2.4.4/debugging) | | Number formats | f64, f32, f16, bf16, FP8 and int8 tensors in `@native` and `@gpu` code | [Number formats](/docs/2.4.4/numbers) | | Long-range graphs | graph transformers, one-step global attention, k-hop and mixed-hop layers | [Graphs](/docs/2.4.4/graphs) | | GPU | one thread per element, automatic CPU/GPU choice, Tensor Cores through cuBLAS | [Native](/docs/2.4.4/native) | | Matrix products | loops written the textbook way run on BLAS: 4–11× faster than C loops | [Performance](/docs/2.4.4/performance#matrix-products-written-as-loops) | | Same speed everywhere | SIMD lanes written by Apsi itself; `@native("fma")` | [below](#16-the-same-speed-on-every-processor) | | Speed overall | 3.84–7.31× faster than 2.0.0 on the same programs (geometric mean) | [below](#how-much-faster-than-200) | ## 1. Python, exactly ### What it does `apsi convert program.py` writes `program.apsi` (and an `.apsi` file for every module next to it). The converted program prints the same text, raises the same errors, writes the same files and trains to the same losses as the Python program. The first line of a converted file is `use pycompat`: it gives the file Python's object model, scoping and built-ins while the text stays Apsi. ```bash apsi convert train.py # train.apsi + the local modules it imports apsi run train.apsi --allow-python -- --epochs 3 # arguments after -- go to sys.argv ``` ### What "exactly" covers Everything in Python 3.13: classes with metaclasses, descriptors, `__slots__`, properties and every special method; dataclasses and enums; `with` and `async with`; `try / except / except* / else / finally`; generators with `send` and `throw`; `async def`, `await`, async comprehensions; `match` with every pattern kind; closures with `global` and `nonlocal`; walrus; star targets; `del`; f-strings with every conversion; generic functions and classes (`def first[T](xs: list[T])`), `type X = …`; `from x import *`; name mangling of `__private` names; names that read before they are bound raise `UnboundLocalError` and `NameError` exactly as in Python. How we check it: `tests/test_stdlib_convert.py` converts **34 pure-Python modules of the standard library itself** (about 38,000 lines: `_pydecimal`, `_pydatetime`, `argparse`, `tomllib`, `json`, `pickle`, `dataclasses`, `enum`, `statistics`, `ipaddress`, `difflib` …) together with a program that uses them, and requires byte-identical output to CPython. `tests/test_python_compat.py` does the same for 16 feature programs, and a 29,022-line AI program with its modules gives identical command output, files, multi-process results and training losses. ### Speed Converted code runs on the same bytecode interpreter as CPython. Object-heavy code is about 8 % faster (Apsi keeps the runtime it loaded out of the cycle collector and collects the youngest generation less often); arithmetic in Python loops runs at CPython's speed. For loops that must be fast, put `@native` on the function: it becomes machine code (see [native code](/docs/2.4.4/native)). ### What differs on purpose * An uncaught error names the `.apsi` file and line; the error type and message are Python's. * The sandbox stays on: files need `--allow-read` / `--allow-write`, libraries such as `torch` need `--allow-python`, and modules that reach the operating system (`subprocess`, `socket`, `ctypes` …) need `--allow-python=all`. `apsi convert` prints the flag a program needs. ## 2. Real time (`use rt`) ```apsi use rt fn control(tick, t): motor.set(pid.update(sensor.read())) print(rt.loop(1000, control, seconds=10)) # rate, lateness percentiles, missed ticks let cam = rt.camera(0, "fhd", fps=60) # or rt.synthetic("1600x900", fps=240) fn process(frame, info): if info.prev_gray is not none: let box = rt.motion(info.prev_gray, rt.gray(frame)) print(rt.video(cam, process, fps=90, seconds=30, adapt=true)) ``` * **Loops:** sleep, then spin on the high-resolution clock for the last 1.5 ms, so ticks start within microseconds. A tick that cannot start on time is counted and skipped instead of making the rhythm drift. * **Video:** the newest frame is always the one processed, so latency never grows; `adapt=true` lowers the working resolution under sustained overload and raises it again; one failing frame never stops the pipeline. * **Sizes:** any width × height from 16 to 16384, and names: `vga`, `svga`, `hd`, `wxga`, `hd+`, `fhd`, `qhd`, `4k` and more (`rt.sizes`). * **Measured** CPU-only on one core of a GitHub runner (AMD EPYC 7763): 640×480 at 708 fps, 1600×900 at 169 fps, 1920×1080 at 106 fps, 3840×2160 at 20 fps; capped runs hold 240/120/90 fps up to 1920×1080 with 1.8–7.6 ms latency. 2000 Hz loops: 0 missed ticks, lateness p99 0.3–0.4 µs. ## 3. RAM, SSD, CPU and GPU (`use memory`) ```apsi use memory print(memory.status()) # RAM, swap, SSD space, cores, load, GPUs memory.budget(16) # GB of RAM for big arrays let x = memory.array([200_000_000, 8], "float32") # RAM if it fits, otherwise a memory-mapped SSD file let y = memory.map(fn(b) => b * 2.0, x) # all cores, larger than RAM is fine let cache = memory.store("embeddings", ram_mb=512, disk_gb=50) let loss, batch = memory.fit_batch(fn(b) => train_step(b), 512) # halves the batch on out-of-memory ``` Spill files and stores live in `~/.apsi` and need no permission; a store at a path you choose needs `--allow-write`. Pickled cache entries are signed, so a file changed outside Apsi is never loaded. ### Smarter memory, later in 2.4.4 `use memory` now knows the container's real limit (cgroup v1/v2), measures memory pressure (`pressure()`, Linux PSI) and reacts by itself — big arrays go to the SSD, stores and pools give RAM back — with `relieve()` and `on_pressure(fn)` for your own reactions; `pool()` reuses big temporaries (1.36×); streaming a 3 GB file peaks at 97 MB instead of 3.1 GB; store writes no longer slow down as the store grows (8.6 → 0.07 ms per `set` at 8,000 entries). See [Memory](/docs/2.4.4/memory). ### More syntax and a collections module, later in 2.4.4 `a ?? b` (a unless it is none), `a?.b`, `a?.f(x)` and `a?[i]` (none when `a` is none, and the rest of the chain is skipped), as in C#, Kotlin and JavaScript. `use collections` adds `Counter`, `DefaultDict`, `Deque`, `Heap`, `LRUCache`, `OrderedSet` and `BiMap`, and 17 functions are built in: `first`, `last`, `partition`, `take_while`, `drop_while`, `scan`, `fold`, `flat_map`, `zip_longest`, `interleave`, `distinct_by`, `min_by`, `max_by`, `sum_by`, `frequencies`, `compose`, `partial`. See [the language reference](/docs/2.4.4/language). ## 4. Fewer AI tokens (`use tokens`) ```apsi use tokens let text = tokens.pack(rows) # a list of records → a compact table assert tokens.unpack(text) == rows # always exact print(tokens.savings(json_str(rows, indent=2), text)) ``` | transform | removes | measured | |---|---|---| | `pack` / `unpack` | repeated JSON keys and quoting | 57–63 % fewer tokens | | `dedupe` / `expand` | repeated lines | 59 % on a log | | `diff` / `patch` | resending an edited document | 99.7 % | Nothing is summarised or approximated: each transform has an exact reverse. Text without redundancy does not shrink, and `tokens.best()` then returns it unchanged. ## 5. Tables (`use dataframe`) ```apsi use dataframe as df let sales = df.read_csv("sales.csv") let top = sales.group_by("customer").agg(total=df.col("amount").sum()).sort("total", descending=true).head(10) print(top.join(df.read_csv("customers.csv"), on="customer", how="left")) ``` Against pandas 2 on 2 million rows: filters 24×, group-by 4.2–4.3×, joins 3.1×, sorting 1.8× faster; 4× less memory. ## 6. Databases (`use db`) ```apsi use db use env let pg = db.postgres(env.secret("DATABASE_URL")) # needs --allow-net=HOST:PORT for row in pg.query("select id, name from item where price < $1", 10): print(row.id, row.name) let r = db.redis("redis://localhost:6379/0") ``` PostgreSQL (SCRAM, TLS, prepared statements, LISTEN/NOTIFY, pools), MySQL / MariaDB (MySQL 8 and 5.7 authentication, TLS), MongoDB (BSON, SCRAM-SHA-256, cursors), Redis (pipelines, pub/sub) and SQLite. Values never become SQL text unescaped. ```apsi let my = db.mysql("mysql://shop:secret@localhost/shop") let items = db.mongo("mongodb://app:secret@localhost/shop").collection("items") ``` ## 7. Web * The HTTP server runs fast routes directly on its event loop and keeps slow or blocking ones on worker threads, judged by CPU time so a busy server is not misled. The work per request is about 16 % lower than in the first 2.4.4 build, uvloop is used when installed (`pip install "apsi[web]"`), and `app.run(processes="auto")` runs one process per CPU. In a synthetic test (a fixed JSON reply over loopback) on a 4-vCPU EPYC 7763: one process 13,700–15,000 requests/s (15,000–16,900 with uvloop), `processes="auto"` 41,700–44,300, Node.js 22's bare `http` module 19,900–25,200 (2.0.0 on the same runner: 5,100–5,800). See [Servers](/docs/2.4.4/server) for what this does and does not measure. * WebSocket routes, rooms and a client: `@app.websocket("/chat")`, `app.room("lobby").send(msg)`, `web.websocket_connect(url)`. ## 8. Configuration and the debugger * `use env`: `.env` files, typed settings (`env.int("PORT", 8080)`), secrets that print as `******`; `apsi run --env-file .env`, `--venv .venv`. * `apsi debug app.apsi` in the terminal, or F5 in VS Code: breakpoints with conditions, hit counts and log points, stepping, variables, tensor summaries, stop on errors. ## 9. Licence The Apsi License 2.0 allows any use, including commercial work, and keeps one rule: Apsi itself may not be sold or offered as a paid service without a licence from ATEC. The programs, libraries and models you build are yours. See [Licensing](/docs/2.4.4/licensing) for examples. ## 10. Faster tools * A cold compile of a 20,000-line program: 3.7 s → 2.5 s; unchanged programs skip the checker (signed marker). * `apsi convert` of 29,022 lines: 4.5 s. ## 11. Hugging Face without Python `use hub` downloads models from the Hugging Face Hub (with a token for private and gated models), tokenizes text (WordPiece, BPE, Unigram) and runs the models natively: embeddings, classification, named entities, fill-mask, and text generation with GPT-2, Llama, Mistral, Qwen and Gemma. See [Hub](/docs/2.4.4/hub). ```apsi use hub let sentiment = hub.pipeline("sentiment-analysis", "distilbert/distilbert-base-uncased-finetuned-sst-2-english") print(sentiment("Apsi is a joy to use")) print(hub.model("HuggingFaceTB/SmolLM2-135M").generate("The three laws of motion are", max_new_tokens=40)) ``` ## 12. Data-parallel training `fit(model, params, X, y, …, workers=4)` trains on four processes: each batch is split into four shards, the gradients are averaged by shard size (the same update as one process) and one optimizer step is taken. ## 13. Matrix products written as loops ```apsi fn matmul_into(a, b, c): for i in 0..a.shape[0]: for k in 0..a.shape[1]: let aik = a[i, k] for j in 0..b.shape[1]: c[i, j] += aik * b[k, j] ``` Loop nests like this one (any loop order, or the dot-product form with `var s = 0.0`) are recognised as a matrix product and run with the tuned BLAS kernel, in the compiled engine and in `@native` code. A 2048×2048 product takes 192 ms (89 GFLOP/s) on a 4-vCPU runner — the same loops in C with `gcc -O3 -march=native` take 2,111 ms, and in Python they would take minutes. When the fast path would not give the loops' exact behaviour (integer arrays, `c` overlapping an input, bounds beyond the arrays) the loops run as written. See [Performance](/docs/2.4.4/performance#matrix-products-written-as-loops) and `benchmarks/matmul`. ## 14. Number formats down to FP8, and a smarter GPU ```apsi @gpu fn matmul(a, b, c): # a, b, c: f16 or bf16 tensors parallel for i in 0..a.shape[0]: for j in 0..b.shape[1]: var acc: f32 = 0.0 # 16-bit data, 32-bit sums for k in 0..a.shape[1]: acc += a[i, k] * b[k, j] c[i, j] = acc ``` * f64, f32, f16, bf16, FP8 (E4M3, E5M2) and int8 tensors work in `@native` and `@gpu` code. Small formats are stored small and computed in f32, with correct rounding when stored ([Number formats](/docs/2.4.4/numbers)). * The loop above gets one GPU thread per element of `c`; with cuBLAS installed it runs as a single GEMM on the Tensor Cores. Launches too small for the GPU run on the CPU automatically. * Outer loops with a reduction inside run four iterations at once in SIMD registers, with bit-identical results ([Performance](/docs/2.4.4/performance#outer-loops-four-at-a-time-exact-simd-for-reductions)). ## 15. Graphs that reach far ```apsi let g = graph(edges=edges, n=n, types=kinds, reverse=true, batch=graph_of_node) let model = graph_transformer_net(16, 64, 2, layers=4, heads=4, relations=g.relations, global_every=2) ``` Message passing moves one hop per layer and squeezes what a node heard into one vector. `graph_transformer` (pre-norm residual neighbour attention, per relation and per hop distance) keeps values alive over many layers; `global_attention` lets every node read any node of its graph in one step; `k_hop` and `mix_hop` read several hops at once; `graph_memory` hands a decoder the most salient nodes of each graph. See [Graphs](/docs/2.4.4/graphs). ## 16. The same speed on every processor `@native` already ran independent outer iterations four at a time ([Performance](/docs/2.4.4/performance#outer-loops-four-at-a-time-exact-simd-for-reductions)), but left the packing into SIMD registers to LLVM. With LLVM 22 that packing failed on AVX2 processors (AMD Zen 2 and 3, Intel without AVX-512 — most desktop PCs): the n-body benchmark took 127 ms on an EPYC 7763, almost three times as long as C. Apsi now writes the vector instructions itself. Every lane still performs exactly its own operations in its own order, so results are bit-for-bit what they were. | n-body (2,000 bodies × 10 steps) | before | now | |---|---:|---:| | AMD EPYC 7763 (Zen 3, AVX2), same runner | 127.5 ms | 46.0 ms (gcc -O3: 44.2 ms) | | Intel Xeon (AVX-512) | 74.5 ms | 70.9 ms | `@native("fma")` is new: every check stays on and the order of operations is kept, but `a * b + c` may become one fused multiply-add, which is what gcc and clang do for C by default. The ODE benchmark (one long chain of dependent arithmetic) takes 288 ms instead of 440 ms, against 387 ms for gcc -O3. Every commit of this work was measured on one machine against the ones before it (`benchmarks/commits/measure.py`, Actions → commits; [results](https://github.com/ATECTechnologies/apsi/blob/main/benchmarks/commits/results-2.4.4.md)). From the start of the work to now, on the same AMD EPYC 7763: n-body **4.0×**, the ODE **1.5×**, k-means **1.27×** and compiling **1.21×** faster, and no row slower. Numbers from different GitHub runs are not comparable with each other: GitHub picks the processor, and the same C program takes 14 ms on one runner model and 44 ms on another. ## How much faster than 2.0.0 The same programs on both versions, on the same machine, with identical results. Measured on a GitHub Actions runner (AMD EPYC, native compiler installed) and on a 2-core VM without the native compiler ([`benchmarks/versions`](https://github.com/ATECTechnologies/apsi/tree/main/benchmarks/versions)): | Program | 2.0.0 | 2.4.4 | runner | 2-core VM ¹ | |---|---:|---:|---:|---:| | matmul 300, loops (compiled engine) | 38,157 ms | 5.0 ms | **7708.43×** | 6374.16× | | 200k objects: create and sum | 3,758 ms | 434 ms | **8.65×** | 9.26× | | fib(25), recursive calls | 82.8 ms | 24.5 ms | **3.37×** | 2.26× | | matmul 512, @native loops | 24.7 ms | 9.6 ms | **2.56×** | 300.48× | | web server, 64 connections | 5,124 req/s | 13,949 req/s | **2.72×** | 3.06× | | web server, 256 connections | 5,848 req/s | 14,052 req/s | **2.4×** | 3.18× | | word count, 260k words | 320 ms | 205 ms | **1.56×** | 1.41× | | start-up: apsi run hello.apsi | 231 ms | 173 ms | **1.34×** | 6.69× | | 200k f-strings and join | 236 ms | 192 ms | **1.23×** | 1.26× | | integer loop, 3M iterations | 171 ms | 168 ms | **1.02×** | 0.99× | | apsi check, 43 example programs (1,594 lines), empty cache | 9,865 ms | 9,526 ms | **1.04×** | 4.9× | | harmonic sum 50M, @native | 70.1 ms | 70.1 ms | **1.0×** | 1.03× | Geometric mean: **3.84× faster** (runner), **7.31×** (2-core VM). `@native` code was already at C speed in 2.0.0 and stays there. Reproduce with `python3 benchmarks/versions/compare.py --old ../apsi-2.0.0 --new . --web`. ¹ Without the native compiler; 2.0.0 also started much more slowly there. ## Upgrading from 2.0.0 Install 2.4.4 over 2.0.0 and run `apsi test`; everything written for 2.0.0 runs unchanged. Python files converted with 2.0.0 can be converted again (`apsi convert file.py`) to get the exact behaviour; a converted module records the Apsi version that wrote it and is converted again automatically when the version changes. # Apsi 2.4.4 — project overview **Apsi** is a programming language for **AI**, **physics** and **servers**. Physical units, tensor-shape checking, automatic differentiation, number formats from FP64 down to FP4, data pipelines and a hardened HTTP server are built into the language. It reads like Python, but catches mistakes before the program runs, and since 2.0 it compiles to **native machine code** through LLVM, running on all CPU cores and on NVIDIA GPUs. ```apsi use physics.constants fn kinetic(m: Mass, v: Velocity) -> Energy = 0.5 * m * v ** 2 print(kinetic(2kg, 3[m/s])) # 9.0 J print(m_e * c ** 2 as [MeV]) # 0.510998949996 MeV let wrong = 5m + 2s # ✗ UnitError before running: cannot add Length and Time @native # machine code for this CPU, units checked then erased fn energy(x, v, k: Float) -> Float: var e = 0.0 parallel for i in 0..len(x): # every CPU core; @gpu runs the same loop on the GPU e += 0.5 * v[i] * v[i] + 0.5 * k * x[i] * x[i] return e ``` ## Install Download from [apsi.dev/download](https://apsi.dev/download) (built yourself: the `dist/` folder). | System | File | |---|---| | Windows 10/11 (x64) | `ApsiSetup-2.4.4-x64.exe` — setup wizard with its own Python; no administrator rights needed | | Any Linux (also WSL) | `sh ApsiSetup-2.4.4-linux.sh` — one-file installer, no root; commands, `.apsi` file type with icon, editor support | | Ubuntu / Debian | `sudo apt install ./apsi_2.4.4_all.deb` | | Any system with Python 3.12+ | `pip install apsi-2.4.4-py3-none-any.whl` | | Code editors | the setup wizard adds Apsi to VS Code, Cursor, Windsurf, VSCodium, Sublime Text, Vim/Neovim and Notepad++ it finds (`apsi editors install` does the same later) | The installer adds `apsi` to your PATH, registers `.apsi` files (icon, *Run with Apsi*, *Edit*, and *New → Apsi source file* in Explorer), creates Start-menu / application-menu entries and adds Apsi to your code editors. Details: [`INSTALL.md`](/docs/2.4.4/install) · [`INSTALL-LINUX.md`](/docs/2.4.4/install-linux) If `apsi` is not recognized right after installing, open a new terminal (quit VS Code completely); other first-run problems and their fixes are in [`TROUBLESHOOTING.md`](/docs/2.4.4/troubleshooting). Without installing: `pip install numpy`, then use `apsi.bat …` (Windows) or `./apsi.sh …` (Linux) from this folder. ### Build from source | | Command | |---|---| | Windows: development setup + tests | `powershell -ExecutionPolicy Bypass -File setup.ps1` | | Windows: + setup wizard | `powershell -ExecutionPolicy Bypass -File setup.ps1 -Installer` | | Linux: development setup + tests | `./setup.sh` | | Linux: + AppImage wizard / all release files | `./setup.sh --installer` / `./setup.sh --release` | | Optional libraries | add `-Libs ai` (Windows) or `--libs ai` (Linux): `gpu`, `onnx`, `torch-auto` (the right PyTorch build), `torch`, `torch-cuda`, `transformers`, `sci`, `data`, `ai`, `ai-gpu`, `science`, `all` | Full details: [`BUILD.md`](/docs/2.4.4/build) ## Your first program ```bash apsi new hello # create a project (templates: app, lib, physics, ai, server) cd hello apsi run # runs src/main.apsi apsi test # runs the tests in tests/ apsi # interactive REPL ``` ## Highlights | Area | What Apsi gives you | |---|---| | **Physical units** | `9.81[m/s^2]`, `2kg`, `5[km]` are checked before the program runs; convert with `as [km]`; every SI unit and prefix plus CODATA constants | | **Tensor shapes** | `fn f[B, T, D](x: Tensor[B, T, D])` — a mismatch names the exact dimension that is wrong | | **Autodiff** | `grad(f)`, `value_and_grad(f)` on tensors, structs and records, unit-aware (`grad(energy)` has the unit of a force); any order: `hessian`, `jacobian`, `laplace`, `grad(grad(f))` — enough for physics-informed neural networks | | **AI** | `use layers`: **any architecture as a graph of layers** (`sequential`, `residual`, `parallel`, `dag`), transformers, CNNs, LSTM/GRU, autoencoders/VAEs, **graph neural networks** (GCN, GraphSAGE, GAT, GIN), a model zoo and `fit()`; plus Adam/AdamW/SGD, schedules, gradient clipping, save/load — reference: [`LAYERS.md`](/docs/2.4.4/layers) | | **GPU training (PyTorch)** | the same models train on PyTorch with `fit(…, backend="torch")` — NVIDIA CUDA, AMD ROCm, Apple MPS, Intel XPU or CPU; fp64/fp32/tf32, bf16/fp16 mixed precision, `torch.compile`, fused attention, cuDNN LSTMs; `torch_module()` gives a real `torch.nn.Module`; `check_torch()` verifies every layer against NumPy; `apsi pkg install torch-auto` picks the right CUDA build — [`BACKENDS.md`](/docs/2.4.4/backends) | | **Python, exactly** | `apsi convert` turns any Python 3.13 program into Apsi that behaves **byte for byte** like the original: classes, metaclasses, dataclasses, `match`, `async`, `except*`, generics, star imports. Verified on 34 standard-library modules (≈38,000 lines) and a 23,769-line AI training program — identical output, files and training losses — [`PYTHON.md`](/docs/2.4.4/python) | | **Real time** | `use rt`: control loops at 1–2 kHz with microsecond lateness; video pipelines at any size from 640×480 to 3840×2160 (named sizes `vga` … `4k`) with newest-frame hand-off, adaptive resolution and per-frame latency reports; CPU or GPU — [`REALTIME.md`](/docs/2.4.4/realtime) | | **RAM, SSD, GPU** | `use memory`: arrays that move to the SSD instead of running out of RAM, out-of-core `chunks`/`map` on all cores, two-tier RAM/SSD caches, batches that shrink on out-of-memory, the GPU with the most free memory — [`MEMORY.md`](/docs/2.4.4/memory) | | **Fewer AI tokens** | `use tokens`: exactly reversible packing for prompts — record lists as tables (−57 to −63 %), repeated lines once (−59 % on logs), only the changed lines of an edited document (−99 %) — [`TOKENS.md`](/docs/2.4.4/tokens) | | **Tables** | `use dataframe`: a columnar engine — filters 24×, group-by 4×, joins 3× faster than pandas, 4× less memory — [`DATAFRAME.md`](/docs/2.4.4/dataframe) | | **Databases** | `use db`: PostgreSQL, Redis and SQLite with no driver to install — [`DATABASES.md`](/docs/2.4.4/databases) | | **Debugger** | breakpoints, stepping, variables and tensors in VS Code (F5) or `apsi debug` — [`DEBUGGING.md`](/docs/2.4.4/debugging) | | **From PyTorch to native** | `apsi convert --idiomatic`: **PyTorch programs become native Apsi** on `use torch`, Apsi's own implementation of the PyTorch API: `nn.Module` classes, every common layer (BatchNorm, LSTM, attention, transformers), losses with all options, optimizers, learning-rate schedulers, `DataLoader`s, `state_dict`s and the training loop. Whole training runs end with the same weights as PyTorch (to 1e-9), with no Python or PyTorch at run time. numpy, math, random and json map onto Apsi too; any other library keeps working through `pyimport` — [`PYTHON.md`](/docs/2.4.4/python) | | **Reinforcement learning** | `use rl`: vectorised environments, GAE, PPO, double DQN, REINFORCE, DPO / IPO / GRPO / RLHF objectives, Monte Carlo tree search, beam search — [`RL.md`](/docs/2.4.4/rl) | | **Libraries** | `.apsil` packages: `apsi install NAME` gets the newest version with dependencies and a lock file, verifies every file's SHA-256 and scans it; `apsi pack` + `apsi publish` with an API token — [`LIBRARIES.md`](/docs/2.4.4/libraries) | | **Science and medicine** | `use special`, `stats`, `signal`, `mechanics`, `pde`, `quantum`, `medical`, `lab`: special functions, distributions and 19 hypothesis tests, regression, power analysis, zero-phase filters and spectra, Lagrangian/Hamiltonian dynamics by autodiff, N-body, orbits, beams and trusses, heat/wave/Poisson/Navier–Stokes solvers, Schrödinger equation and qubits, pharmacokinetics, survival analysis (Kaplan–Meier, log-rank, Cox), ROC/DeLong, ECG/HRV, DICOM, reproducible experiment logs. Verified against SciPy, statsmodels, lifelines, scikit-learn and pydicom — [`SCIENCE.md`](/docs/2.4.4/science) | | **Physics** | `sci.solve_ivp` with an implicit TR-BDF2 solver for **stiff** systems and adaptive RK45; `evolve` solves differential equations with euler / midpoint / rk4 / adaptive **rk45** / symplectic / verlet; Laplacians for PDEs; linalg (eigh, solve, …); measurement uncertainty `9.81 ± 0.02` that propagates through formulas with correct correlations | | **Native code** | `@native` compiles a function to machine code through LLVM for the exact CPU (AVX2/AVX-512), specialised on argument types and units; on par with C, C++ and Rust and 100–3,500× faster than the 1.x bytecode engine on loops ([benchmarks](/docs/2.4.4/native#performance)); overflow, index and division checks stay on by default; results identical to the bytecode engine | | **Large models** | `apsi plan 1T --active 32B` / `use scale`: memory per GPU, tensor × pipeline × data layout, GPU count, days and serving memory for 1B–2T-parameter models (dense or mixture-of-experts, BF16/FP8) — [`SCALE.md`](/docs/2.4.4/scale) | | **Parallel & GPU** | `parallel for` runs iterations on all CPU cores with deterministic reductions (bit-identical for any thread count); `@gpu` runs it on every NVIDIA GPU in the machine (GTX 1660 and newer, multi-GPU split, no CUDA toolkit) and falls back to all CPU cores — [`NATIVE.md`](/docs/2.4.4/native) | | **Speed (rest of the language)** | the compiler turns the whole program into bytecode; every function has an "erased twin" without unit bookkeeping for hot loops; parsed programs are kept in a signed compile cache; tensor work runs on multi-threaded BLAS, on the GPU through CuPy, and neural networks through PyTorch | | **Executables** | `apsi build app.apsi` → `app.exe` (Windows) / `app` (Linux) with the program, its imports, Apsi as bytecode and fixed permissions; `--standalone` adds its own Python runtime for PCs without Apsi | | **Precision** | `@precision(exact)` computes with exact fractions; `@precision(f32)` / `f16` for AI | | **Security** | permission system (Python, C, network are off by default), Apsi Guard pre-run scan, and a `crypto` module (scrypt, HMAC, SHA-3, tokens, constant-time compare) | | **Other languages** | Python/PyTorch (`pyimport`, `to_torch`, `torch_module`), C/C++ (`cload`, zero-copy tensors), JS/Next.js/Java through `apsi serve` (JSON HTTP API) | | **Number formats** | FP64, FP32, FP16, BF16, FP8 (E4M3/E5M2), FP6, FP4, INT8, INT4 — bit-exact with current GPU formats, stored at their real size, quantization-aware training | | **Data** | streaming pipelines: parallel map, prefetch, shuffle, vectorized batching (~2 M rows/s), sliding windows, sharding, interleaving; CSV, JSON Lines, Parquet, `.npy`; memory-mapped tensors on SSD, signed on-disk cache | | **Accuracy** | exact fractions, correctly rounded sums (`fsum`), measurement uncertainty, unit checking, bit-exact low-precision formats | | **Concurrency** | tasks, channels, locks, atomics, `parallel_for` / `parallel_reduce`, `pmap`, BLAS thread control | | **Servers** | `use web`: asyncio protocol + worker pool + multi-process, 20,000+ req/s on 2 vCPU at 50–1,000 connections; WebSocket routes, rooms and client; `.env` files and secrets (`use env`); protection against slowloris, request smuggling, path traversal, header injection and DoS; JWT, API keys, rate limits, CORS, TLS, metrics | | **AI models** | `use models`: Apsi checkpoints (`.apsim`: units, compressed tensors, SHA-256, signatures, sharding), GGUF (bit-exact Q8_0/Q4_0), safetensors read/write, memory-mapped loading, INT8/FP8/FP4 compression (4–16× less RAM), `qlinear`; ONNX Runtime, Hugging Face and PyTorch models served as an API in a few lines | | **Tooling** | `run`, `check`, `test`, `fmt`, `watch`, `bench`, `build`, `compile`, `convert`, `new`, `task`, `doc`, `lsp`, `repl`, `serve`, `doctor`, `editors`, `pkg`; extensions for VS Code, Cursor, Windsurf, VSCodium, Sublime Text, Vim/Neovim and Notepad++ with live diagnostics, unit hovers, go to definition, tasks, snippets and themes; web playground (WebAssembly) | | **Platforms** | Windows 10/11 and Linux installers; CI on Windows Server 2022/2025 and Ubuntu 22.04/24.04; Python 3.12 and newer (3.12, 3.13, 3.14) | ## Commands ```text apsi interactive REPL (cd, ls, run FILE, !cmd and :help work inside it) apsi FILE.apsi [ARGS] run a program (same as apsi run) apsi -e "CODE" evaluate code and print the result apsi run [FILE] [ARGS] check, then run (default: the project's main file); --watch, --time apsi check [PATHS] check files or folders without running them apsi test [PATHS] run every test "..." block apsi fmt [PATHS] format code (--check for CI) apsi watch [PATHS] re-run, re-test or re-check on every save apsi bench FILE time a program over several runs apsi build FILE make an executable: app.exe (Windows) / app (Linux); --standalone, --allow-… apsi compile [PATHS] compile programs ahead of time into the signed compile cache (--stats, --clear) apsi new NAME / apsi init create a project with apsi.toml, src/, tests/ and VS Code tasks apsi task [NAME] / apsi NAME run a task from apsi.toml apsi doc [NAME] documentation for built-ins and modules apsi audit / scan / verify permissions a program needs / malware scan / installation integrity apsi serve FILE expose a program's functions as a JSON HTTP API apsi install NAME install an Apsi library (.apsil) from the registry: NAME@^1.2, a file, or [dependencies] apsi pack / apsi publish build dist/NAME-VERSION.apsil / upload it with an API token apsi search WORDS · apsi lib list | info | registry | login the library registry apsi pkg install torch-auto optional Python libraries (torch-auto, gpu, onnx, transformers, ai, …) apsi config --profile strict default security profile apsi completion powershell shell completion (bash, zsh, fish, powershell) apsi editors install Apsi support in VS Code, Cursor, Windsurf, VSCodium, Sublime, Vim/Neovim, Notepad++ apsi convert app.py translate Python (and whole folders) into Apsi apsi native info | explain FILE the native compiler: CPU, SIMD, GPUs; which functions run as machine code apsi run --native FILE compile every function that allows it to machine code apsi plan 1T --active 32B plan training of a large model: GPUs, layout, memory per GPU, days apsi lsp | doctor | ast | tokens | version ``` Every command and flag is described in the *Command line* chapter of [`LANGUAGE.md`](/docs/2.4.4/language). ## Examples (`examples/`) | File | Shows | |---|---| | `01_hello` | language basics: let/var, functions, structs, comprehensions, pipes | | `02_units` | physical units, conversions, constants | | `03_autodiff` | derivatives, force from potential energy, linear regression | | `04_projectile` | projectile with air drag (rk4) | | `05_orbit` | one year of Earth's orbit; energy drift only 1e-14 (verlet) | | `06_heat_equation` | 2-D heat equation | | `07_quantum_box` | Schrödinger equation; energy levels match theory | | `08_neural_net` | MLP trained from scratch to 99 % accuracy | | `09_transformer` | character-level mini-GPT written from scratch | | `10_cnn` | convolutional network | | `11_precision` | exact / f32 / memo | | `12_security` | passwords, HMAC, hashing, sandbox | | `13_interop` | Python / PyTorch / C | | `14_api_server` | HTTP API for Next.js / Java | | `15_native_c` | calling your own compiled C without copying data | | `16_nbody` | vectorized gravity for 200 bodies | | `17_wave` | wave on a string returns to its shape after one period | | `18_pinn` | physics-informed network learns cos(ωt) from the equation alone | | `19_measurement` | measurement uncertainty + rk45 | | `20_spectrum` | FFT finds a signal's frequency (in Hz) | | `21_curve_fit` | fit a decay to get τ ± σ; minimize, root finding, integration | | `22_lstm_sequence` | LSTM time-series prediction | | `23_web_api` | API server: AI model, orbit simulation, JWT login, rate limit | | `24_low_precision` | FP64 → FP4/INT4 comparison + quantization-aware training | | `25_data_pipeline` | parallel SSD data pipeline + prefetch + disk_cache | | `26_model_serving` | train → safetensors → INT8 load → API | | `27_pretrained_models` | Hugging Face / ONNX → API | | `28_any_architecture` | `use layers`: residual MLP, a home-made DAG, a graph attention network, a small GPT | | `29_stiff_chemistry` | stiff Robertson kinetics and an RC circuit with `sci.solve_ivp` | | `30_gpu_training` | training on the GPU through the PyTorch backend | | `31_native_speed` | `@native` machine code, units at zero cost, fused tensor kernels, `parallel for` on all cores | | `32_gpu` | `@gpu` kernels: SAXPY, a reduction and a matrix multiply on the GPU(s), or all CPU cores without one | | `33_model_files` | Apsi checkpoints (units, compression, SHA-256, signatures, sharding) and GGUF export | | `34_statistics` | a two-group study: descriptive statistics, Welch t, Mann–Whitney, power, regression, FDR, tables | | `35_mechanics` | a double pendulum from its Lagrangian, an exact pendulum period, a Hohmann transfer, a cantilever | | `36_quantum` | harmonic-oscillator states, a moving wave packet, a Bell pair and its entanglement | | `37_medical` | pharmacokinetics and NCA, Kaplan–Meier, log-rank, Cox regression, diagnostic accuracy, eGFR | | `38_reinforcement_learning` | PPO learns CartPole; Monte Carlo tree search finds the winning move | | `39_pytorch_converted` | a PyTorch script with BatchNorm, AdamW, cosine schedule, label smoothing and clipping after `apsi convert` | ## Tests ```bash apsi test examples tests/apsi --allow-all # test blocks written in Apsi (pass on --engine compile and interp) python -m unittest discover tests # toolchain, REPL, CLI, LSP, formatter, Windows/Linux, web server ``` ## Development Development started on **Sunday, 8 November 2026 at about 04:00** (Bangkok time, UTC+7). Everything from the first line to this release, Apsi 2.4.4, took about **25 hours of active work** in sessions over several days. That number is final: it is the first build of the project, completed with 2.4.4; later versions are new work. ## Documentation - [`WHATS-NEW.md`](/docs/2.4.4/whats-new) — **everything new in 2.4.4**, with examples, measurements and limits - [`GETTING-STARTED.md`](/docs/2.4.4/getting-started) — install, first program, first model, first library - [`NATIVE.md`](/docs/2.4.4/native) — **native code**: `@native`, `parallel for`, `@gpu`, benchmarks against C, C++, Rust, Go and Mojo - [`LIBRARIES.md`](/docs/2.4.4/libraries) — `.apsil` libraries: install, create, pack, publish, the registry API - [`RL.md`](/docs/2.4.4/rl) — reinforcement learning and learning from preferences - [`SCALE.md`](/docs/2.4.4/scale) — planning 21B–2T-parameter training runs (`apsi plan`, `use scale`) - [`TROUBLESHOOTING.md`](/docs/2.4.4/troubleshooting) — `apsi` not recognized, VS Code without colours, `needs permission 'python'` - [`LANGUAGE.md`](/docs/2.4.4/language) — **the complete language reference**: syntax, types, units, functions, structs, modules, every built-in, the REPL and the command line - [`AI.md`](/docs/2.4.4/ai) — building AI with Apsi: any architecture, smaller RAM/VRAM, pretrained models, serving - [`LAYERS.md`](/docs/2.4.4/layers) — **`use layers` reference**: every layer with shapes and parameters, `dag`, graph neural networks, the model zoo, `fit`, recipes - [`BACKENDS.md`](/docs/2.4.4/backends) — NumPy and PyTorch backends: CUDA, ROCm, Apple MPS, Intel XPU, bf16/fp16 mixed precision, `torch.compile`, `torch.nn.Module` export - [`PYTHON.md`](/docs/2.4.4/python) — converting Python and PyTorch programs with `apsi convert` - [`REALTIME.md`](/docs/2.4.4/realtime) — 1–2 kHz loops and video from 640×480 to 4K (`use rt`) - [`MEMORY.md`](/docs/2.4.4/memory) — RAM budgets, SSD spill, out-of-core data, OOM-safe batches (`use memory`) - [`DATAFRAME.md`](/docs/2.4.4/dataframe) — tables (`use dataframe`) - [`DATABASES.md`](/docs/2.4.4/databases) — PostgreSQL, Redis, SQLite (`use db`) - [`DEBUGGING.md`](/docs/2.4.4/debugging) — the debugger - [`ENV.md`](/docs/2.4.4/env) — `.env` files, secrets, Python virtual environments - [`TOKENS.md`](/docs/2.4.4/tokens) — fewer LLM tokens with exactly reversible transforms (`use tokens`) - [`SERVER.md`](/docs/2.4.4/server) — secure, high-throughput servers - [`PERFORMANCE.md`](/docs/2.4.4/performance) — number formats, CPU, GPU, PyTorch, RAM, SSD - [`INTEROP.md`](/docs/2.4.4/interop) — Python, PyTorch, C/C++, JS/Next.js, Java - [`SECURITY.md`](/docs/2.4.4/security) — permissions, Apsi Guard, profiles - [`EDITORS.md`](/docs/2.4.4/editors) — VS Code, Cursor, Windsurf, VSCodium, Sublime Text, Neovim/Vim, Notepad++, JetBrains, Helix, Zed, Emacs - [`INSTALL.md`](/docs/2.4.4/install) · [`INSTALL-LINUX.md`](/docs/2.4.4/install-linux) · [`BUILD.md`](/docs/2.4.4/build) - [`PLAYGROUND.md`](/docs/2.4.4/playground) — the web playground - `ROADMAP.md` · [`CHANGELOG.md`](https://github.com/ATECTechnologies/apsi/blob/main/CHANGELOG.md) ## Source layout ``` apsi/ the language lexer.py text → tokens (indentation, Unicode, unit literals) parser.py tokens → syntax tree checker.py names, units, argument counts and evolve rules, checked before running interpreter.py reference tree-walking runtime native/ the native compiler (LLVM): compiler.py (specialiser, units, fusion, parallel for → IR), engine.py (JIT, -O3 for the host CPU, signed machine-code cache, thread pool), __init__.py (@native, calling convention), gpu.py + cuda.py (@gpu: PTX, multi-GPU, CUDA driver) codegen.py Apsi → Python bytecode compiler + erased twins compiler.py closure compiler for evolve in the interpreter engine cli.py the apsi command repl.py the interactive REPL project.py apsi new / init / task and apsi.toml editors.py apsi editors: integrations for VS Code-family editors, Sublime, Vim/Neovim, Notepad++ convert.py apsi convert: Python → Apsi translator with library mapping layers.py use layers: composable layers, DAG models, GNNs, model zoo, fit runner.py runs programs on a large stack (deep recursion), forwards Ctrl+C tools.py apsi eval / watch / bench / doc / completion integrators.py rk45 (Dormand–Prince) uncertain.py measurement uncertainty and error propagation sci.py einsum, FFT, integrate, find_root, minimize, curve_fit formatter.py apsi fmt lsp.py language server playground.py runner for the web playground precision.py BF16/FP8/FP6/FP4/INT8/INT4 data.py data pipelines and SSD storage concurrency.py tasks, channels, parallel loops web.py HTTP server models.py safetensors, model compression, ONNX / Hugging Face / PyTorch modelio.py GGUF and Apsi checkpoints (.apsim), apsi model special.py stats.py dsp.py mechanics.py pde.py quantum.py physics.py medical.py lab.py science, engineering and medical modules (docs/SCIENCE.md) convert_torch.py PyTorch programs -> native Apsi (used by apsi convert) units.py SI dimensions and the Quantity type autodiff.py reverse-mode autodiff (incl. conv2d and attention ops) ops.py math/tensor operations for numbers, tensors, quantities and autodiff nodes stdlib.py built-ins and the nn / physics / linalg / random modules interop.py Python, PyTorch, C FFI, files, network (through the permission system) crypto.py security module security.py permissions, profiles, release manifest guard.py Apsi Guard pre-run threat scan hardware.py apsi doctor and the GPU backend server.py apsi serve examples/ 39 examples (+ native/: C code and build scripts for Windows/Linux) tests/ test suite (tests/test_native.py: native vs bytecode differential tests) benchmarks/ the same benchmarks in Apsi, C, C++, Rust and Go (sh benchmarks/run_all.sh); heavy/: eight large workloads also in Mojo (python3 benchmarks/heavy/run.py) docs/ documentation editors/ editor integrations: vscode/ (VS Code, Cursor, Windsurf, VSCodium), sublime/, vim/ (Vim + Neovim), notepad++/, textmate/ (JetBrains, TextMate), helix/, zed/, emacs/ install/ script installers for Windows (install.ps1 / install.bat) and Linux (install.sh) installer/ the setup wizard (Electron + React) playground/ browser playground packaging/ release builds (.exe, .deb, zip, wheel) tools/ playground build and test scripts .github/ CI on Windows and Linux ``` Test the playground in real WebAssembly: ```bash npm install pyodide@0.27.7 && node tools/test_playground.mjs playground/apsi_bundle.js ``` ## License and credit Apsi is developed by **ATEC** and distributed under the [Apsi License 2.0](https://github.com/ATECTechnologies/apsi/blob/main/LICENSE): free to use for any purpose, including commercial work; the programs, libraries and models you build with Apsi are yours to license and sell; Apsi itself may not be sold or offered as a paid service without a licence from ATEC; copies and changed versions of Apsi credit ATEC and stay under the same licence. Plain-language guide with examples: [`LICENSING.md`](/docs/2.4.4/licensing). # Licensing: what you may do with Apsi Apsi is published under the **Apsi License 2.0** (the `LICENSE` file). This page explains it in plain words with examples. It is a guide, not the licence itself and not legal advice: if anything here differs from `LICENSE`, the licence applies, and for a decision that matters to your business, ask a lawyer. ## The short version * **Use Apsi for anything** — study, research, hobby, school, government, and commercial work in companies. Free. * **Sell what you build with Apsi** — programs, libraries, apps, models, services. They are yours. * **Do not sell Apsi itself** — no paid copies, paid downloads, or paid hosted Apsi without a licence from ATEC. * **Credit ATEC** when you share Apsi or a changed version of it, and keep changed versions of Apsi open under the same licence. ## Allowed or not? Examples | You want to… | Allowed? | Why | |---|---|---| | Learn Apsi, use it at university, teach a course with it | ✅ | any purpose is allowed (section 3.1) | | Use Apsi at your company to build internal tools or analyse data | ✅ | use inside an organisation is not selling (section 2) | | Write an app in Apsi and sell it, including as an executable from `apsi build` | ✅ | your programs are yours; the runtime may travel with them (sections 2, 6) | | Run a paid web service whose back end is written in Apsi | ✅ | you are selling your program, not Apsi | | Train an AI model with Apsi and sell the model or its predictions | ✅ | models and results belong to you (section 6) | | Publish an Apsi library on apsi.dev under MIT, GPL, or a paid licence | ✅ | you choose the licence of your libraries (section 5.3) | | Convert your company's Python code with `apsi convert` and keep it closed | ✅ | converted programs are your programs | | Charge clients for consulting, development, training or support that uses Apsi | ✅ | charging for your own work is not selling the Software | | Fork Apsi, improve it and share the fork for free | ✅ | keep the licence, credit ATEC, say what you changed, use your own name | | Sell copies of Apsi or a fork of Apsi, or charge for downloading it | ❌ | that is Selling the Software (section 5.1) — ask ATEC for a licence | | Offer a paid online Apsi compiler, notebook or IDE | ❌ | hosted Apsi where Apsi is what customers pay for | | Build a paid language or compiler on top of Apsi's source code | ❌ | a Modified Version sold for money | | Remove the credit or call your fork "Apsi" | ❌ | sections 5.2 and 5.4 | | Put a fork under a different, more restrictive licence | ❌ | Modified Versions stay under this licence (section 5.3) | Not sure? Ask yourself: *are people paying for **Apsi**, or for **what I made with it**?* The first needs a licence from ATEC; the second is yours. ## What to write where * **Sharing Apsi or a fork** — keep `LICENSE` and add this to your README: ```text Based on Apsi, originally developed by ATEC — https://github.com/ATECTechnologies/apsi Licensed under the Apsi License 2.0. Apsi itself may not be sold; programs written in Apsi are yours. ``` * **Shipping your program with the Apsi runtime** (for example an executable from `apsi build`) — one line in your documentation, licence file or "about" box: ```text Uses the Apsi runtime by ATEC (https://apsi.dev), Apsi License 2.0. ``` * **Programs and libraries you write in Apsi** — nothing is required. Use any licence you like. ## Contributions Pull requests to the official repository are welcome. By sending one you confirm that you may contribute it, and you allow ATEC to include it in Apsi under this licence and under ATEC's commercial licences, so that Apsi keeps one licence. You keep the copyright of your work (section 8). ## Patents ATEC grants a patent licence for using Apsi. It ends for anyone who sues claiming that Apsi infringes a patent (section 4). ## If something goes wrong If you break a condition, ATEC notifies you in writing and you have 30 days to fix it — for example by adding the missing credit or taking down a paid offer. The first time, your rights come back as soon as you comply (section 9). ## Older versions Apsi 1.10.0 and 2.0.0 were released under the Apsi Non-Commercial License 1.0. You may use them under the Apsi License 2.0 instead (section 13), which allows more. ## Why the licence looks like this The licence was written after comparing the approaches that source-available projects use: * **Non-commercial licences** such as the PolyForm Noncommercial License 1.0.0 allow only non-commercial use. That blocks companies from simply *using* the software, which is too narrow for a programming language. Apsi allows all use, and restricts only selling Apsi itself. * **Delayed open-source licences** such as the Business Source License 1.1 restrict production use and convert to an open-source licence on a change date. Apsi does not restrict production use at all. * **Patent clauses.** The patent licence and patent defence follow the common pattern in PolyForm and Apache-style licences. * **Thai law.** The Copyright Act B.E. 2537 (1994) protects computer programs. Its exceptions (sections 32 and 35, for example study, research, back-up copies and adapting a program you lawfully use) stay available. Under the Unfair Contract Terms Act B.E. 2540 (1997), a party cannot exclude its own liability for fraud or gross negligence in advance, and courts can limit unfair terms. That is why the disclaimer applies "to the extent the applicable law allows" and leaves that liability in place. ## Sources * [PolyForm Noncommercial License 1.0.0 (SPDX)](https://spdx.org/licenses/PolyForm-Noncommercial-1.0.0.html) * [Business Source License 1.1 (SPDX)](https://spdx.org/licenses/BUSL-1.1.html) * [Copyright Act B.E. 2537 (1994), Department of Intellectual Property, Thailand](https://ipthailand.go.th/images/26669/2566/laws/COPYRIGHT%20ACT%20B.E.%202537%20(1994).pdf) * [Technology licensing in Thailand — Tilleke & Gibbins](https://www.tilleke.com/insights/technology-licensing-thailand) # Apsi 2.4.4 — everything on one page This page is the whole language in one place: every piece of syntax with a short example, then **every built-in function, every standard module and every member, and every command** of Apsi 2.4.4, generated from the runtime itself so nothing is missing. Read it top to bottom once, then use it with your browser's search (Ctrl+F). The long-form explanations live in the [language reference](/docs/2.4.4/language). ## How a program looks ```apsi use physics.constants # modules: use NAME / use NAME as n import "helpers.apsi" as h # another file of yours # comments start with # let g = 9.81[m/s^2] # let = constant, units in [ ] or after a number: 2kg, 3.5s var t = 0s # var = variable fn fall(h: Length) -> Time = sqrt(2 * h / g) # one-line function with unit-checked types print(f"10 m takes {fall(10m):.3f}") # f-strings with format specs ``` Blocks are indented (4 spaces), like Python. Lines may continue inside ( ), [ ], { }. ## Values | Write | Type | Notes | |---|---|---| | `42`, `1_000_000`, `-7` | `Int` | any size | | `3.14`, `6.02e23`, `inf`, `nan` | `Float` | 64-bit IEEE | | `2kg`, `9.81[m/s^2]`, `1.5[kWh]` | quantity | number + SI unit; wrong units are an error before running | | `3.0 ± 0.1`, `x +/- dx` | uncertain | errors propagate through every formula | | `exact("1/3")` | exact | rational numbers without rounding | | `true`, `false`, `none` | `Bool`, none | | | `"text"`, `f"x = {x:.2f}"` | `Str` | `+`, `*`, slicing, `.upper() .split() .replace() .strip() .find() …` | | `[1, 2]`, `(1, 2)`, `{"k": 1}`, `set([1, 2])` | `List`, `Tuple`, `Dict`, `Set` | Python methods: `.append .pop .get .items .keys …` | | `0..10`, `0..=10`, `0s..5s` | `Range` | end excluded / included; ranges with units | | `tensor([[1, 2], [3, 4]])`, `zeros(3, 3)`, `randn(64)` | `Tensor` | NumPy-style arrays, may carry a unit; `@` is matrix product | | `record(W=w, b=b)` | `Record` | named fields, no declaration | | `fn(x) => x * 2` | `Fn` | functions are values | ## Variables and operators ```apsi let a = 1 # cannot be reassigned var b = 2 # can: b = 3, b += 1 (also -= *= /= //= %= **= @=) let x, y = (1, 2) # unpacking a + b a - b a * b a / b a // b a % b a ** b a @ b # arithmetic, matrix product a == b a != b a < b a <= b a ~= b # ~= approximately equal (also checks units) a and b a or b not a x in xs # logic, membership a if cond else b data |> normalize |> train # conditional, pipe xs[0] xs[-1] xs[1:3] xs[::2] A[:, 0] A[A > 0] # indexing and slicing ``` ## Control flow ```apsi if x > 0: print("positive") elif x == 0: print("zero") else: print("negative") while n > 1: n //= 2 for i in 0..10: # also lists, strings, dict keys, tensors, files, channels if i == 3: continue if i == 7: break for i, item in enumerate(items): pass parallel for i in 0..n: # iterations on all CPU cores (native) out[i] = work(i) assert total > 0, "total must be positive" let squares = [x * x for x in 0..10 if x % 2 == 0] let pairs = [(i, j) for i in 0..3 for j in 0..3 if i != j] ``` ## Errors ```apsi try: let cfg = load_json("config.json") except FileNotFoundError, PermissionError: print("using defaults") except ValueError as e: # e.kind, e.message, e.line print("bad config:", e.message) else: print("loaded") finally: print("done") raise ValueError("speed must be positive") raise "plain message" # a RuntimeError ``` Kinds use Python's names (`ValueError`, `KeyError`, `IndexError`, `ZeroDivisionError`, `TypeError`, `OSError`, `FileNotFoundError` …) plus `UnitError` and `ShapeError`; errors Apsi raises itself match them too. ## Generators ```apsi fn naturals(): var n = 0 while true: yield n # values are produced one at a time; endless is fine n += 1 ``` ## Functions ```apsi fn area(w, h = 1.0) = w * h # one expression fn stats(xs: List) -> Tuple: # a block let m = mean(xs) return m, std(xs) area(2.0, h=3.0) # keyword arguments let double = fn(x) => x * 2 # lambda fn attention[B, T, D](q: Tensor[B, T, D], k: Tensor[B, T, D]) -> Tensor[B, T, T]: return q @ transpose(k, (0, 2, 1)) / sqrt(D) # shapes are checked at every call ``` ```apsi fn total(*xs, **opts) = sum(xs) * opts.get("scale", 1) # any number of arguments total(1, 2, 3) total(*nums, **options) # spread a list / dict into a call ``` Decorators: `@memo` (cache results), `@native` (compile to machine code), `@native("fast")`, `@precision(f32)` / `@precision(exact)`. ## Structs (your own types) ```apsi struct Particle: pos: Tensor[3, Length] vel: Tensor[3, Velocity] mass: Mass = 1kg fn kinetic(self) -> Energy = 0.5 * self.mass * dot(self.vel, self.vel) let p = Particle(tensor([0.0, 0, 0]) * u.m, tensor([1.0, 0, 0]) * u.m / u.s) print(p.kinetic()) # 0.5 J ``` Methods named `add sub mul div eq lt le gt ge neg len get set contains iter to_str call hash bool` make `+ - * / == < … len() [] in for print()` work on your structs (Python's `__add__`, `__str__` … under friendlier names). ## Physical units `2kg`, `3.5[m/s]`, `9.81[m/s^2]`, `1[kWh]`, `90deg` — every SI unit and prefix. Adding a length to a time, or passing a mass where a `Length` is declared, is reported **before the program runs**. `d as [km]` converts for display; `magnitude(x)` gives the plain number in SI units; `unit(x)` and `dim(x)` describe it. Type names such as `Length`, `Mass`, `Time`, `Velocity`, `Force`, `Energy`, `Power`, `Pressure` are used in annotations. ## Differential equations (`evolve`) ```apsi var x = 1m var v = 0[m/s] evolve x, v over t in 0[s]..10[s] step 1[ms] using rk4 record every 10 into traj: x' = v v' = -4[N/m] / 1kg * x ``` Methods: `euler`, `midpoint`, `rk4`, `rk45` (adaptive, `tol 1e-10`), `symplectic`, `verlet`. ## Gradients `grad(f)(x)`, `value_and_grad(f)(x)`, `hessian(f)`, `jacobian(f)`, `derivative(f, x)` — on numbers, tensors, quantities (the gradient has the right units) and parameter trees (records, structs, lists, dicts). ## Tests ```apsi test "falling takes longer from higher": assert fall(20m) > fall(10m) ``` `apsi test` runs every `test` block of a file, folder or project. ## Permissions Programs run sandboxed. Files, network, environment variables and Python need explicit flags: `--allow-read[=DIRS]`, `--allow-write[=DIRS]`, `--allow-net[=HOSTS]`, `--allow-env`, `--allow-python[=safe|all]`, or `--allow-all`. `apsi audit app.apsi` prints exactly the flags a program needs. Pure standard modules (`pyimport("collections")`, heapq, itertools, re, datetime …) need no flag. ## Built-in functions and values (298) Available everywhere without `use`. Grouped by first letter; the signature shows parameters (`=…` means optional). ### A | Name | What it is | |---|---| | `abs(x)` | absolute value (element-wise for tensors; keeps units) | | `acos(x)` | inverse cosine, radians | | `all(iterable, /)` | Return True if bool(x) is True for all values x in the iterable. | | `any(iterable, /)` | Return True if bool(x) is True for any x in the iterable. | | `approx(a, b, rel=1e-06, atol=1e-12)` | true when a and b are equal within a relative and absolute tolerance (also `a ~= b`) | | `arange(start, stop=None, step=1)` | evenly spaced values in [start, stop) with a step, as a tensor | | `argmax(x, axis=None)` | index of the largest value (along an axis) | | `argmin(x, axis=None)` | index of the smallest value (along an axis) | | `args()` | Command-line arguments after the program name: `apsi run app.apsi a b` -> ["a", "b"]. | | `argsort(xs, reverse=False)` | function | | `ArithmeticError(/, *args, **kwargs)` | Base class for arithmetic errors. | | `ascii(obj, /)` | Return an ASCII-only representation of an object. | | `asin(x)` | inverse sine, radians | | `AssertionError(/, *args, **kwargs)` | Assertion failed. | | `atan(x)` | inverse tangent, radians | | `atan2(y, x)` | angle of the point (x, y), radians | | `AttributeError(/, *args, **kwargs)` | Attribute not found. | ### B | Name | What it is | |---|---| | `band(a, b)` | `a & b` of Python: bits, set intersection, element-wise and | | `bf16` | value | | `bf8` | value | | `bin(number, /)` | Return the binary representation of an integer. | | `bnot(a)` | `~a` | | `bool(/, *args, **kwargs)` | true/false of a value | | `bor(a, b)` | `a | b` of Python: bits, set union, dict merge, element-wise or | | `bxor(a, b)` | `a ^ b` of Python | | `bytearray(/, *args, **kwargs)` | bytearray(iterable_of_ints) -> bytearray bytearray(string, encoding[, errors]) -> bytearray bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer bytearray(int) -> bytes array of size given by the parameter initi | | `bytes(/, *args, **kwargs)` | bytes(iterable_of_ints) -> bytes bytes(string, encoding[, errors]) -> bytes bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer bytes(int) -> bytes object of size given by the parameter initialized with null byte | ### C | Name | What it is | |---|---| | `callable(obj, /)` | Return whether the object is callable (i.e., some kind of function). | | `cbrt(x)` | cube root | | `ceil(x)` | round up to an integer | | `chain_lists(xss)` | join a list of lists into one list | | `chars(s)` | the characters of a string as a list | | `chr(i, /)` | Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff. | | `chunks(xs, n)` | function | | `clamp(x, lo, hi)` | function | | `classmethod(/, *args, **kwargs)` | Convert a function to be a class method. | | `clip(x, lo, hi)` | limit values to [lo, hi] | | `cload(path)` | load a C library (needs --allow-ffi) | | `clock()` | time since the program started, in seconds (a Quantity) | | `combinations(xs, k)` | function | | `complex(/, *args, **kwargs)` | Create a complex number from a string or numbers. | | `concat(items, axis=0)` | join tensors along an axis | | `ConnectionError(/, *args, **kwargs)` | Connection error. | | `cos(x)` | cosine (radians or an angle quantity) | | `cosh(x)` | hyperbolic cosine | | `count_by(xs, key=None)` | function | | `cross(a, b)` | cross product of 3-vectors | | `cstr` | value | | `cumsum(x, axis=None)` | running sum | ### D | Name | What it is | |---|---| | `dequantize(q)` | turn a quantized tensor back into floats | | `derivative(f, x)` | numerical derivative of a function at a point | | `dict(/, *args, **kwargs)` | make a dictionary | | `dim(x)` | the physical dimension of a value (length, time …) | | `dir(obj=None)` | function | | `divmod(x, y, /)` | Return the tuple (x//y, x%y). Invariant: div*y + mod == x. | | `dot(a, b)` | dot product | ### E | Name | What it is | |---|---| | `e` | constant | | `e4m3` | value | | `e5m2` | value | | `einsum(spec, *operands)` | Einstein summation, e.g. einsum("bij,bjk->bik", A, B). Units multiply; differentiable. | | `enumerate(xs, start=0)` | pairs (index, item) | | `EnvironmentError(/, *args, **kwargs)` | Base class for I/O related errors. | | `EOFError(/, *args, **kwargs)` | Read beyond end of file. | | `erf(x)` | error function | | `error(msg)` | stop the program with an error message | | `Error(/, *args, **kwargs)` | An error raised from Apsi code with `raise "message"` or `raise Error("message")`. | | `exact(x)` | switch to exact rational arithmetic in a block or for a value | | `Exception(/, *args, **kwargs)` | An error raised from Apsi code with `raise "message"` or `raise Error("message")`. | | `exists(path)` | true when a file exists (needs read permission) | | `exit(code=0)` | Stop the program with an exit status (0 = success). | | `exp(x)` | e to the power x | | `expm1(x)` | function | | `eye(n)` | identity matrix | ### F | Name | What it is | |---|---| | `f16` | value | | `f32` | value | | `f64` | value | | `FileExistsError(/, *args, **kwargs)` | File already exists. | | `FileNotFoundError(/, *args, **kwargs)` | File not found. | | `filter(f, xs)` | items of a list for which a function returns true | | `flatten(x)` | tensor as one dimension | | `float(x)` | convert to a floating-point number | | `FloatingPointError(/, *args, **kwargs)` | Floating-point operation failed. | | `floor(x)` | round down to an integer | | `flush_out()` | function | | `format(value, format_spec='', /)` | Return type(value).__format__(value, format_spec) | | `format_info(fmt)` | details of a number format (f16, bf16, f32 …) | | `fp16` | value | | `fp32` | value | | `fp4` | value | | `fp4_e2m1` | value | | `fp6` | value | | `fp64` | value | | `fp6_e2m3` | value | | `fp6_e3m2` | value | | `fp8` | value | | `fp8_e4m3` | value | | `fp8_e5m2` | value | | `from_py(x)` | Python/JSON value -> Apsi value ({value, unit} dicts become Quantities). | | `from_torch(t)` | turn a PyTorch tensor into an Apsi tensor | | `frozenset(/, *args, **kwargs)` | an unchangeable set | | `fsum(x)` | Correctly rounded sum (no accumulated rounding error), for lists and tensors of any size. | | `full(shape, value)` | tensor filled with one value | ### G | Name | What it is | |---|---| | `gelu(x, approximate='tanh')` | GELU. approximate="tanh" (default, fast) or "none" (exact: x·Φ(x), as PyTorch's default). | | `getattr(obj, name, *default)` | getattr(obj, "field"[, default]); names starting with _ are never reachable. | | `getenv(name, default=None)` | Read an environment variable (needs --allow-env); the place for secrets like API keys. | | `gpu(*args, **kwargs)` | run work on the GPU when one is available | | `gpu_info()` | The GPUs Apsi can use (needs the NVIDIA driver; no CUDA toolkit). | | `grad(f, wrt=0)` | gradient of a function (automatic differentiation) | | `group_by(xs, key)` | function | ### H | Name | What it is | |---|---| | `hasattr(obj, name)` | true when a value has a field or method | | `hash(x)` | A stable hash for numbers, strings, tuples and struct instances; tensors hash their bytes. | | `hessian(f)` | hessian(f)(x) -> matrix of second derivatives of a scalar f. | | `hex(number, /)` | Return the hexadecimal representation of an integer. | | `http_get(url, timeout=30)` | download a URL (needs --allow-net) | | `http_post_json(url, data, timeout=30)` | POST JSON to a URL (needs --allow-net) | | `hypot(*xs)` | √(Σ x²) without overflow/underflow; works with quantities of the same unit and with tensors. | ### I | Name | What it is | |---|---| | `i32` | value | | `i64` | value | | `id(obj, /)` | Return the identity of an object. | | `ImportError(/, *args, **kwargs)` | Import can't find module, or can't find name in module. | | `IndexError(/, *args, **kwargs)` | Sequence index out of range. | | `inf` | constant | | `input(prompt='')` | Read one line from standard input (without the newline); returns none at end of input. | | `int(x, base=None)` | convert to an integer | | `int4` | value | | `int8` | value | | `IOError(/, *args, **kwargs)` | Base class for I/O related errors. | | `IsADirectoryError(/, *args, **kwargs)` | Operation doesn't work on directories. | | `isfinite(x)` | function | | `isinstance(x, t)` | type test | | `isnan(x)` | function | | `issubclass(cls, class_or_tuple, /)` | Return whether 'cls' is derived from another class or is the same class. | | `iter(…)` | iter(iterable) -> iterator iter(callable, sentinel) -> iterator | ### J | Name | What it is | |---|---| | `jacobian(f)` | jacobian(f)(x) -> d f_i / d x_j (f: Tensor[M] -> Tensor[M], x: Tensor[N]) as Tensor[M, N]. | | `join(xs, sep='')` | join strings with a separator | | `json_parse(text)` | parse JSON text into lists, dicts, numbers and strings | | `json_str(value, indent=None, sort_keys=False, ensure_ascii=False, separators=None, default=None)` | turn a value into JSON text (indent, sort_keys …) | ### K | Name | What it is | |---|---| | `KeyError(/, *args, **kwargs)` | Mapping key not found. | | `keys(d)` | keys of a dict as a list | ### L | Name | What it is | |---|---| | `laplace(f)` | laplace(f)(x) -> Σ ∂²f/∂x_i² (trace of the Hessian), e.g. for PDE residuals. | | `len(x)` | number of items | | `lerp(a, b, t)` | function | | `library_path(path)` | The actual file cload() would load (e.g. vecmath -> vecmath.dll on Windows). | | `linspace(start, stop, n=50)` | n evenly spaced values from start to stop | | `list(x=())` | make a list from any sequence | | `load_csv(path, skip_header=0)` | read a CSV file into columns/rows | | `load_json(path)` | read a JSON file | | `log(x)` | natural logarithm | | `log10(x)` | base-10 logarithm | | `log1p(x)` | function | | `log2(x)` | base-2 logarithm | | `log_softmax(x, axis=-1)` | log of softmax, numerically stable | | `logsumexp(xs, axis=None)` | function | | `LookupError(/, *args, **kwargs)` | Base class for lookup errors. | ### M | Name | What it is | |---|---| | `magnitude(x)` | the number without its unit (in SI base units) | | `map(f, xs)` | apply a function to every item | | `matmul(a, b)` | matrix product (also `a @ b`) | | `max(*args, axis=None, keepdims=False, key=None, default=…)` | largest value (of a list/tensor, or of the arguments) | | `mean(x, axis=None, keepdims=False, start=None)` | average | | `measure(value, sigma)` | value ± sigma -> an independent measurement with standard uncertainty sigma. | | `median(xs)` | function | | `memo(fn)` | cache a function's results (@memo) | | `MemoryError(/, *args, **kwargs)` | Out of memory. | | `meshgrid(x, y)` | coordinate grids from 1-D axes | | `min(*args, axis=None, keepdims=False, key=None, default=…)` | smallest value | | `mode(xs)` | function | | `ModuleNotFoundError(/, *args, **kwargs)` | Module not found. | ### N | Name | What it is | |---|---| | `NameError(/, *args, **kwargs)` | Name not found globally. | | `nan` | constant | | `nan_to_num(x, nan=0.0, posinf=None, neginf=None)` | function | | `native(*args, **kwargs)` | compile a function to machine code (@native) | | `native_info()` | What the native compiler sees on this machine (CPU, SIMD, threads, cache). | | `native_threads(n=None)` | Number of CPU threads `parallel for` and large tensor kernels use (set it with an argument). | | `nbytes(x)` | memory a tensor uses, in bytes | | `ndim(x)` | number of tensor dimensions | | `next(…)` | next(iterator[, default]) | | `nominal(x)` | central value of an uncertain quantity | | `norm(x, axis=None)` | Euclidean (or other) norm | | `NotImplementedError(/, *args, **kwargs)` | Method or function hasn't been implemented yet. | ### O | Name | What it is | |---|---| | `object(/, *args, **kwargs)` | The base class of the class hierarchy. | | `oct(number, /)` | Return the octal representation of an integer. | | `ones(*shape, dtype=None)` | tensor of ones | | `ones_like(x)` | ones with the shape of another tensor | | `open(file, mode='r', *args, **kwargs)` | function | | `ord(character, /)` | Return the ordinal value of a character. | | `OSError(/, *args, **kwargs)` | Base class for I/O related errors. | | `outer(a, b)` | outer product | | `OverflowError(/, *args, **kwargs)` | Result too large to be represented. | ### P | Name | What it is | |---|---| | `pairwise(xs)` | function | | `param_count(tree)` | number of trainable numbers in a model | | `PermissionError(/, *args, **kwargs)` | Not enough permissions. | | `permissions()` | the permissions the program runs with | | `permutation(n, seed=None)` | random permutation of 0..n | | `permutations(xs, k=None)` | function | | `permute(x, *dims)` | Reorder the axes (PyTorch's x.permute(...)). | | `pi` | constant | | `platform()` | "windows", "linux" or "macos". | | `pmap(f, xs, workers=None)` | Parallel map on a thread pool (numpy/BLAS work releases the GIL). | | `pow(base, exp, mod=None)` | Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments | | `precision(dtype)` | @precision(f32) / @precision(exact): numeric precision of a function. | | `print(*args, sep=' ', end='\n', flush=False, file=None)` | write values to the output, separated by spaces | | `prod(x, axis=None)` | product of all values | | `product(*lists, repeat=1)` | function | | `property(/, *args, **kwargs)` | Property attribute. | | `ptr_f32` | value | | `ptr_f64` | value | | `ptr_i32` | value | | `ptr_i64` | value | | `py_type_name(x)` | The name Python would give the type of x (int, float, str, list, dict, or the struct's name). | | `pyimport(name)` | use a Python module (pure ones like collections need no permission) | ### Q | Name | What it is | |---|---| | `qmatmul(a, b)` | Matrix multiply with quantised operands: decode to FP32, multiply, accumulate in FP32 (the same numerics as low-precision tensor-core GEMMs). | | `quant_error(x, fmt, axis=None)` | Relative RMS error introduced by storing x in `fmt`. | | `quantize(x, fmt, scale='auto', axis=None)` | Quantise a tensor. scale="auto" maps max\|x\| to the format's largest value (per tensor, or per channel along `axis`); scale=1.0 keeps raw values (no scaling). | ### R | Name | What it is | |---|---| | `rand(*shape, seed=None)` | uniform random numbers in [0, 1) | | `randint(lo, hi, *shape, seed=None)` | random integers | | `randn(*shape, seed=None)` | normally distributed random numbers | | `range(a, b=None, step=1)` | integers start..stop with a step (like Python) | | `read_text(path)` | read a text file | | `record(**kw)` | a lightweight object with named fields | | `RecursionError(/, *args, **kwargs)` | Recursion limit exceeded. | | `relative_uncertainty(x)` | uncertainty divided by the value | | `relu(x)` | max(x, 0) | | `repr(x)` | text form of a value as code would write it | | `reshape(x, *shp)` | same data, new shape | | `reversed(xs)` | items in reverse order | | `roll(x, shift, axis=None)` | shift values cyclically | | `round(x, digits=0)` | round to n digits | | `round_to(x, fmt, scale='auto', axis=None)` | Round x to what the format can represent and return it as a normal tensor ("fake quantisation"). Gradients pass straight through, so this works inside training. | | `RuntimeError(/, *args, **kwargs)` | Unspecified run-time error. | ### S | Name | What it is | |---|---| | `safe_div(a, b, default=0.0)` | function | | `save_csv(path, data, header='')` | write rows to a CSV file | | `save_json(path, value)` | write a value as a JSON file | | `security_policy()` | the active security profile | | `seed(n)` | make random numbers reproducible | | `set(/, *args, **kwargs)` | a set: unique items, fast `in` | | `set_recursion_limit(n)` | Allow deeper recursion (Python's sys.setrecursionlimit), capped at 1 000 000; the stack grows with it. | | `setattr(obj, name, value)` | function | | `shape(x)` | shape of a tensor | | `shl(a, b)` | `a << b` | | `shr(a, b)` | `a >> b` | | `sigma(x)` | standard uncertainty of a value | | `sigmoid(x)` | 1 / (1 + e^-x) | | `sin(x)` | sine (radians or an angle quantity) | | `sinh(x)` | hyperbolic sine | | `slice(/, *args, **kwargs)` | slice(stop) slice(start, stop[, step]) | | `softmax(x, axis=-1)` | normalised exponentials (probabilities) | | `softplus(x)` | log(1 + e^x) | | `sort(x)` | sorted copy of a tensor | | `sorted(xs, key=None, reverse=False)` | sorted copy of a list (key=, reverse=) | | `sqrt(x)` | square root (units become their square root) | | `squeeze(x, axis=None)` | Remove axes of length 1 (only `axis` when given, and only if it has length 1, as in PyTorch). | | `stack(items, axis=0)` | stack tensors along a new axis | | `staticmethod(/, *args, **kwargs)` | Convert a function to be a static method. | | `std(x, axis=None)` | standard deviation | | `stop_gradient(x)` | use a value without differentiating through it | | `StopIteration(/, *args, **kwargs)` | Signal the end from iterator.__next__(). | | `str(x)` | text form of a value | | `sum(x, axis=None, keepdims=False, start=None)` | sum of all values (or along an axis) | | `super(/, *args, **kwargs)` | super() -> same as super(__class__, ) super(type) -> unbound super object super(type, obj) -> bound super object; requires isinstance(obj, type) super(type, type2) -> bound super object; requires issubcla | | `swapaxes(x, a, b)` | Exchange two axes (PyTorch's x.transpose(a, b)). | ### T | Name | What it is | |---|---| | `tan(x)` | tangent | | `tanh(x)` | hyperbolic tangent | | `tau` | constant | | `tensor(x, dtype=None)` | make a tensor from numbers or nested lists (dtype=…) | | `testing()` | true while `apsi test` runs | | `timed(fn)` | @timed: print how long each call takes. | | `timeit(f, repeat=5, warmup=1)` | Run f() repeat times after warm-up runs; seconds as a record(best, median, mean, runs). | | `TimeoutError(/, *args, **kwargs)` | Timeout expired. | | `to_float(x)` | plain float from a quantity or 1-element tensor | | `to_py(x)` | Apsi value -> plain Python/JSON-friendly value (units become {value, unit}). | | `to_torch(x, device=None)` | turn an Apsi tensor into a PyTorch tensor (needs --allow-python) | | `transpose(x, axes=None)` | swap tensor axes | | `tree_map(f, *trees)` | apply a function to every leaf of nested lists/dicts/records | | `tuple(/, *args, **kwargs)` | make a tuple | | `type(v)` | type name of a value ("Int", "Tensor" …) | | `TypeError(/, *args, **kwargs)` | Inappropriate argument type. | ### U | Name | What it is | |---|---| | `u` | `u.km`, `u.eV` ... -> one of that unit as a Quantity. | | `uint8` | value | | `UnicodeDecodeError(/, *args, **kwargs)` | Unicode decoding error. | | `UnicodeEncodeError(/, *args, **kwargs)` | Unicode encoding error. | | `UnicodeError(/, *args, **kwargs)` | Unicode related error. | | `unique(xs)` | distinct values | | `unit(x)` | the unit of a quantity | | `unsqueeze(x, axis)` | Insert an axis of length 1 (negative axes count from the end, as in PyTorch). | ### V | Name | What it is | |---|---| | `value_and_grad(f, wrt=0)` | value and gradient of a function (automatic differentiation) | | `ValueError(/, *args, **kwargs)` | Inappropriate argument value (of correct type). | | `var(x, axis=None)` | variance | | `vars(obj)` | The fields of a struct or record as a dict. | | `void` | value | ### W | Name | What it is | |---|---| | `where(cond, a, b)` | choose element-wise: where(cond, a, b) | | `windows(xs, n)` | function | | `write_err(text)` | function | | `write_out(text)` | Write text without a line break (Python's sys.stdout.write). | | `write_text(path, text)` | write a text file (needs write permission) | ### Z | Name | What it is | |---|---| | `ZeroDivisionError(/, *args, **kwargs)` | Second argument to a division or modulo operation was zero. | | `zeros(*shape, dtype=None)` | tensor of zeros | | `zeros_like(x)` | zeros with the shape of another tensor | | `zip(*xs)` | pairs items of several lists | ## Standard modules (43) Load a module with `use NAME` (all its names) or `use NAME as n` (then `n.member`). ### `use crypto` — 11 names | Name | What it is | |---|---| | `b64decode(text)` | function | | `b64encode(text)` | function | | `hash(data, algo='sha256')` | function | | `hmac_sign(key, message, algo='sha256')` | function | | `hmac_verify(key, message, signature, algo='sha256')` | function | | `password_hash(password, n=16384, r=8, p=1)` | Salted password hash to store in a database. | | `password_verify(password, stored)` | function | | `random_hex(nbytes=32)` | function | | `random_token(nbytes=32)` | function | | `secure_equal(a, b)` | Constant-time comparison (prevents timing attacks). | | `secure_randint(lo, hi)` | Uniform integer in [lo, hi) from the OS CSPRNG. | ### `use data` — 28 names | Name | What it is | |---|---| | `batch(p, size, drop_last=False)` | function | | `cache(p, directory=None, shard=4096)` | Cache a pipeline. directory=none -> RAM; otherwise memory-mapped shards on disk. | | `disk_cache(directory='cache')` | @disk_cache("cache"): memoise a function's results on disk across runs. | | `filter(p, f)` | function | | `flush(x)` | function | | `from_csv(path, skip_header=0, chunk=10000, delimiter=',')` | function | | `from_files(pattern)` | function | | `from_generator(f)` | f() is called at the start of every epoch and must return an iterable. | | `from_jsonl(path)` | function | | `from_list(xs)` | function | | `from_npy(path)` | function | | `from_parquet(path, columns=None, rows=65536)` | Stream a Parquet file in record batches: each item is a dict of column -> tensor. | | `from_range(n)` | function | | `from_tensors(*arrays, batch=None, shuffle=False, seed=None, drop_last=False)` | Rows of one or more equal-length tensors: from_tensors(X, y) yields (x_i, y_i). | | `interleave(pipelines, cycle=True)` | Alternate items from several pipelines (round robin) until all are exhausted. | | `load_tensor(path, mmap=False)` | function | | `map(p, f, workers=None, ordered=True)` | Apply f to every item; workers > 1 runs f in parallel (numpy work releases the GIL). | | `open_tensor(path, shape, dtype='f32', mode='w+')` | A memory-mapped tensor on disk (SSD): larger than RAM, written/read lazily. | | `prefetch(p, n=2)` | Produce up to n items ahead on a background thread (overlaps loading and compute). | | `repeat(p, times=None)` | function | | `save_tensor(path, x)` | function | | `shard(p, count, index)` | Every `count`-th item starting at `index` — split one dataset across workers or machines. | | `shuffle(p, buffer=1024, seed=None)` | Streaming shuffle with a bounded buffer (a full shuffle when buffer >= dataset size). | | `skip(p, n)` | function | | `take(p, n)` | function | | `throughput(p, limit=None)` | Measure items per second of a pipeline (optionally only the first `limit` items). | | `unbatch(p)` | function | | `window(p, size, shift=1, drop_last=True)` | Sliding windows over a stream: each item is a tensor of `size` consecutive items (time series). | ### `use dataframe` — 12 names | Name | What it is | |---|---| | `col(name)` | function | | `concat(frames)` | function | | `count()` | function | | `DataFrame(data=None, columns=None)` | type | | `Expr(fn, name, agg=None)` | A column expression, evaluated against a DataFrame: col("a") * 2 > col("b"). | | `frame(data=None, columns=None)` | type | | `from_csv_text(text, sep=',', columns=None)` | function | | `from_records(rows, columns=None)` | function | | `lit(v)` | function | | `read_csv(path, sep=',', columns=None, interp=None)` | function | | `read_json(path, interp=None)` | A JSON array of objects, or an object of columns → a DataFrame. | | `read_jsonl(path, interp=None)` | One JSON object per line (JSON Lines / NDJSON) → a DataFrame (missing keys become nulls). | ### `use db` — 10 names | Name | What it is | |---|---| | `DatabaseError(msg, code=None, detail=None)` | An error reported by the database server (has .code: the SQLSTATE for PostgreSQL). | | `mariadb(url=None, **kw)` | function | | `mongo(url=None, **kw)` | function | | `mysql(url=None, **kw)` | function | | `pool(url, size=8, **kw)` | function | | `postgres(url=None, **kw)` | function | | `redis(url=None, **kw)` | function | | `RedisError(msg, code=None, detail=None)` | type | | `Result(rows=(), columns=(), status='', rowcount=0)` | Rows of a query (a list), with .columns, .rowcount (rows affected) and .status ("INSERT 0 3" …). | | `sqlite(path=':memory:')` | function | ### `use env` — 12 names | Name | What it is | |---|---| | `bool(name, default=False)` | function | | `float(name, default=None)` | function | | `get(name, default=None)` | function | | `has(name)` | function | | `int(name, default=None)` | function | | `list(name, sep=',', default=None)` | function | | `load(path='.env', override=True, export=False)` | Read a .env file (needs --allow-read for it). Returns the names it defined. | | `parse(text)` | Parse .env text into a dict (the same rules as python-dotenv / docker compose). | | `require(*names)` | function | | `secret(name, default=None)` | function | | `Secret(value, name='')` | A value that does not show itself: print, str, f-strings and logs give ******. | | `values` | value | ### `use ffi` — 6 names | Name | What it is | |---|---| | `cload(path)` | function | | `ptr_f32` | value | | `ptr_f64` | value | | `ptr_i32` | value | | `ptr_i64` | value | | `void` | value | ### `use gpu` — 8 names | Name | What it is | |---|---| | `cpu_threads()` | function | | `device()` | "cuda:0" when a GPU meeting Apsi's target tier is usable through CuPy, otherwise "cpu". | | `gpu_available()` | function | | `gpu_info()` | GPU 0: name, compute capability, memory. Works with CuPy, or with just the NVIDIA driver. | | `hardware()` | function | | `to_cpu(x)` | function | | `to_device(x, dev=None)` | function | | `to_gpu(x)` | function | ### `use hp` — 36 names | Name | What it is | |---|---| | `acos(x)` | function | | `asin(x)` | function | | `atan(x)` | function | | `atan2(y, x)` | function | | `cbrt(x)` | function | | `cos(x)` | function | | `cosh(x)` | function | | `det(a)` | function | | `digits(n=None)` | Set (or read) the number of significant digits of hp results (default 50, at most 100 000). | | `dot(a, b)` | function | | `e()` | function | | `equal(a, b, digits_=None)` | True when a and b agree to `digits` significant digits (default: the current precision minus 2). | | `exp(x)` | function | | `factorial(n)` | function | | `float(x)` | function | | `gamma(x)` | Γ(x) for positive integers and half-integers (exact formulas). | | `inv(a)` | function | | `ln(x)` | function | | `log(x)` | function | | `log10(x)` | function | | `log2(x)` | function | | `matmul(a, b)` | function | | `mean(xs)` | function | | `num(x)` | An hp number. Strings keep every digit you write ("0.1" is exactly one tenth); floats keep their exact binary value (0.1 becomes 0.1000000000000000055511…), integers and fractions are exact. | | `pi()` | π to the current number of digits (Chudnovsky-free: Machin's formula with binary splitting of atan). | | `pow(x, y)` | function | | `prod(xs)` | function | | `root(x, n)` | The real n-th root (negative x allowed for odd n). | | `sin(x)` | function | | `sinh(x)` | function | | `solve(a, b)` | x with a·x = b, by LU factorisation with partial pivoting at the current precision (+ guard digits). | | `sqrt(x)` | function | | `str(x, n=None)` | x as text with exactly n significant digits (default: the current precision). | | `sum(xs)` | The exact sum (no rounding error accumulates), rounded once to the current precision. | | `tan(x)` | function | | `tanh(x)` | function | ### `use hub` — 18 names | Name | What it is | |---|---| | `cache_dir()` | function | | `clear_cache(repo=None)` | function | | `config(repo, revision='main', token=None)` | function | | `download(repo, filename, revision='main', token=None, kind='model', progress=True)` | One file → its local path (cached by commit; resumed; SHA-256 checked for large files). | | `files(repo, revision=None, token=None, kind='model')` | function | | `HubError(msg, line=None, col=None)` | type | | `info(repo, revision=None, token=None, kind='model')` | function | | `load(repo, filename=None, revision='main', token=None, dtype=None)` | Weights as {name: tensor}: model.safetensors (also sharded), or a named .safetensors / .gguf file. | | `login(token)` | function | | `logout()` | function | | `model(repo, revision='main', token=None)` | Download what a model needs and load it to run natively (see Model). | | `model_from_folder(folder)` | Load a model that runs natively from a local folder in Hugging Face layout. | | `offline(on=True)` | function | | `pipeline(task, repo, revision='main', token=None)` | A function for one task (sentiment-analysis, feature-extraction, ner, fill-mask, text-generation). | | `search(query='', task=None, limit=20, sort='downloads')` | function | | `snapshot(repo, patterns=('*.json', '*.safetensors', '*.txt', '*.model'), revision='main', token=None, kind='model')` | Download the files that match the patterns; returns the folder. | | `tokenizer(repo, revision='main', token=None)` | function | | `tokenizer_from_file(path)` | function | ### `use io` — 9 names | Name | What it is | |---|---| | `exists(path)` | function | | `json_parse(text)` | function | | `json_str(value, indent=None, sort_keys=False, ensure_ascii=False, separators=None, default=None)` | function | | `load_csv(path, skip_header=0)` | function | | `load_json(path)` | function | | `read_text(path)` | function | | `save_csv(path, data, header='')` | function | | `save_json(path, value)` | function | | `write_text(path, text)` | function | ### `use lab` — 9 names | Name | What it is | |---|---| | `experiment(name, params=None, folder='experiments')` | Start an experiment log. Returns a record with log(step, metrics), note(text), result(metrics), finish() and the run id. Every call appends one JSON line to FOLDER/NAME.jsonl. | | `format_measurement(value, uncertainty, unit=None)` | Value with its standard uncertainty in parenthesis notation, rounded by the usual rule (uncertainty to two significant digits when they start with 1 or 2, otherwise one): 9.8123 ± 0.0312 -> "9.81(3)". | | `load_runs(folder='experiments', name=None)` | All runs in an experiment folder (or one experiment): list of record(run, name, params, logs, result, notes, provenance, finished). | | `provenance()` | What produced a result: software versions, machine and time (no user names, host names or paths). | | `set_seed(seed)` | Seed every random generator Apsi uses (Apsi's random module, numpy's legacy and Python's random). | | `sigfig(x, n=3)` | Round to n significant figures. | | `table_csv(rows, headers=None)` | CSV text (RFC 4180 quoting). | | `table_latex(rows, headers=None, caption=None, label=None)` | A LaTeX table (booktabs) from lists or records; text is escaped. | | `table_markdown(rows, headers=None)` | A Markdown table from lists or records. | ### `use layers` — 78 names | Name | What it is | |---|---| | `activation(fn)` | function | | `add()` | function | | `autoencoder(d_in, latent, hidden=(64,), act='relu')` | Returns a DAG with outputs [reconstruction, code]. | | `avg_pool(k=2)` | type | | `batch_norm_layer(d, eps=1e-05)` | Normalises over the batch (and spatial) axes with the current batch's statistics. | | `bilinear(n1, n2, n_out)` | type | | `check_torch(*a, **k)` | Compare every layer type on NumPy and PyTorch (outputs and gradients, float64). | | `checkpoint(layer)` | Gradient checkpointing: checkpoint(layer) gives the same results with much less activation memory. | | `cnn(in_channels, classes, image=28, channels=(16, 32), dense_width=64)` | Small convolutional classifier for images shaped [N, C, H, W]. | | `concatenate()` | function | | `conv2d_layer(c_in, c_out, k=3, stride=1, padding='same')` | type | | `count_params(params)` | function | | `custom_layer(init, forward, name='model', parts=None)` | A layer from your own functions: init(seed) returns the parameter tree, forward(p, x, train) the output. | | `dag(nodes, inputs=('x',), output=None)` | function | | `dense(n_in, n_out, act=None)` | function | | `devices()` | The compute devices available: NumPy, and PyTorch CPU / CUDA / ROCm / MPS / XPU when installed. | | `dropout_layer(p=0.1)` | type | | `eccentricity(g, undirected=True, samples=64, seed=0)` | The longest shortest path (in hops) seen from up to `samples` start nodes: how far information must travel. | | `embedding(vocab, d)` | type | | `fit(model, params, X, y=None, backend='numpy', **kw)` | Train `model`. Returns record(params, history, val_history, device). | | `flatten_layer(/, *args, **kwargs)` | type | | `gat(n_in, n_out, heads=4, concat=True, slope=0.2)` | Graph attention (Veličković et al.) with several heads; heads are concatenated (or averaged). | | `gat_net(n_in, hidden, classes, heads=4, dropout=0.3)` | function | | `gather(x, ids)` | Rows of x at ids (differentiable). | | `gcn(n_in, n_out, bias=True)` | Graph convolution (Kipf & Welling): h_i = Σ_j x_j W / sqrt(deg_i deg_j). | | `gcn_net(n_in, hidden, classes, layers=2, dropout=0.5)` | function | | `gin(n_in, n_out, hidden=None)` | Graph isomorphism network layer: MLP((1 + eps) x_i + Σ_j x_j). | | `gin_net(n_in, hidden, classes, layers=3, pool='sum')` | Graph-level classifier: GIN layers, pooling per graph, linear head. | | `global_attention(d, heads=4, max_keys=4096)` | Pre-norm residual self-attention over all nodes of the same graph: any node reaches any other in one step. | | `global_avg_pool(/, *args, **kwargs)` | type | | `graph(edges=None, n=None, src=None, dst=None, self_loops=True, undirected=False, batch=None, types=None, rever…)` | graph(edges=[[0, 1], [1, 2]], n=3) or graph(src=tensor, dst=tensor, n=…). `batch` assigns nodes to separate graphs for graph-level tasks (used by graph_pool). `types` gives every edge a relation (0, 1, …) for graph_trans | | `graph_memory(d, k=16)` | The k most salient node states of every graph, weighted by a learned score: [graphs, k, d]. A decoder or any other model reads a whole graph through these k vectors with cross-attention (multi_head_attention([q, mem])) i | | `graph_pool(how='mean')` | Node features -> one vector per graph (mean, sum or max over each graph's nodes). | | `graph_transformer(d, heads=4, relations=1, ff=None, max_hop=16, activation='gelu')` | Pre-norm residual graph block: x + attention over neighbours (by relation and hop), then x + MLP. | | `graph_transformer_net(n_in, d, out, layers=4, heads=4, relations=1, global_every=2, max_keys=4096, task='node', pool='mean')` | Input projection, `layers` graph_transformer blocks with a global_attention block after every `global_every`-th one (0: none), a final layer norm, then a linear output per node (task="node") or per graph (task="graph", p | | `gru_layer(n_in, n_hidden, return_sequences=True)` | type | | `hop_distances(g, source, undirected=True)` | Number of hops from `source` to every node (-1 when unreachable), by breadth-first search. | | `identity(/, *args, **kwargs)` | type | | `k_hop(g, k, undirected=False)` | A graph with an edge u -> v for every v within 1..k hops of u (edge.hop = the distance), plus self loops. One layer over this graph reads k hops at once; graph_transformer learns a separate bias per distance. | | `lambda_layer(fn, name='lambda', torch_fn=None)` | type | | `last_step(/, *args, **kwargs)` | type | | `layer_norm_layer(d, eps=1e-05)` | type | | `linear(n_in, n_out, bias=True, init='he')` | function | | `load_torch_weights(model, params, state, prefix='')` | Copy a PyTorch state_dict (or a flat record/dict of arrays with the same names, e.g. from load_safetensors(path, nested=false)) into an Apsi parameter tree of the same architecture. Linear weights are transposed, LSTM/GR | | `lstm_classifier(n_in, hidden, classes, layers=1)` | function | | `lstm_layer(n_in, n_hidden, return_sequences=True)` | type | | `max_pool(k=2)` | type | | `mean_pool(axis=1)` | Mean over one axis, e.g. over time: [B, T, D] -> [B, D]. | | `mix_hop(n_in, n_out, hops=(0, 1, 2))` | Several hop distances in parallel: concat_k (Â^k x) W_k with  the mean over incoming neighbours. | | `mlp(sizes, act='relu', dropout=0.0, norm=False, out_act=None)` | Multilayer perceptron, e.g. mlp([2, 64, 64, 3]). | | `multi_head_attention(d, heads=4, causal=False)` | type | | `multiply()` | function | | `parallel(branches, combine='concat')` | function | | `params_from_torch(*a, **k)` | Parameters of a module made by torch_module (after training it in Python) as an Apsi parameter tree. | | `positional_encoding(max_len, d, learned=False)` | type | | `predict(model, params, X, backend='numpy', **kw)` | function | | `reshape_layer(*shape)` | type | | `residual(inner, pre_norm=None)` | function | | `resnet_mlp(d_in, d_out, width=64, blocks=3, act='gelu')` | Deep MLP with residual blocks and layer norm — trains stably at any depth. | | `rms_norm_layer(d, eps=1e-06)` | type | | `sage(n_in, n_out, aggregate='mean')` | GraphSAGE: h_i = x_i W_self + mean_{j→i} x_j W_neigh. | | `scale(d, shift=True)` | A learnable per-feature scale (and shift). | | `segment_count(ids, n)` | function | | `segment_max(data, ids, n)` | Per-segment maximum (no gradient through the choice of maximum). | | `segment_mean(data, ids, n)` | function | | `segment_softmax(scores, ids, n)` | Softmax of `scores` within each segment (e.g. over the incoming edges of every node). | | `segment_sum(data, ids, n)` | out[k] = sum of data[i] where ids[i] == k (k < n). Differentiable in `data`. | | `select(i)` | Pick one element of a list/tuple input (e.g. the output of parallel). | | `sequential(layers)` | function | | `siamese(encoder, combine='concat', head=None)` | Shared encoder applied to [a, b]; the outputs are combined (concat/add/mul/list) and optionally fed to head. | | `summary(model, params=None)` | A table of the model's layers and parameter counts (printed, and returned as a string). | | `torch_module(*a, **k)` | The model with its parameters as a torch.nn.Module: module(x) / module(x, graph=g). | | `transformer_block(d, heads=4, ff=None, causal=False, dropout=0.0, activation='gelu')` | Pre-norm transformer block: x + attention(norm(x)), then x + MLP(norm(x)). | | `transformer_classifier(vocab, classes, d=64, heads=4, layers=2, context=128)` | Encoder for sequences of token ids [B, T] -> class logits [B, classes] (mean pooling). | | `transformer_encoder(d, heads=4, layers=2, ff=None, causal=False, dropout=0.0)` | function | | `transformer_lm(vocab, d=64, heads=4, layers=2, context=128, dropout=0.0)` | Decoder-only (GPT-style) language model: token ids [B, T] -> logits [B, T, vocab]. | | `vae(d_in, latent, hidden=(64,), act='relu')` | Variational autoencoder. Returns [reconstruction, mu, logvar]; train with vae_loss. | | `vae_loss(outputs, x, beta=1.0)` | function | ### `use linalg` — 8 names | Name | What it is | |---|---| | `det(a)` | function | | `eig(a)` | function | | `eigh(a)` | function | | `inv(a)` | function | | `norm(x, axis=None)` | function | | `solve(a, b)` | function | | `svd(a)` | function | | `trace(a)` | function | ### `use math` — 35 names | Name | What it is | |---|---| | `abs(x)` | function | | `acos(x)` | function | | `asin(x)` | function | | `atan(x)` | function | | `atan2(y, x)` | function | | `besselj(n, x)` | Bessel function of the first kind J_n(x), integer n. | | `beta(a, b)` | B(a, b) = Γ(a)Γ(b)/Γ(a+b). | | `cbrt(x)` | function | | `ceil(x)` | function | | `comb(n, k)` | Binomial coefficient C(n, k) (exact for integers). | | `cos(x)` | function | | `cosh(x)` | function | | `e` | constant | | `erf(x)` | function | | `erfc(x)` | function | | `erfinv(y)` | Inverse error function. | | `exp(x)` | function | | `factorial(n)` | n! (exact integer for integers). | | `floor(x)` | function | | `gamma(x)` | Γ(x). | | `inf` | constant | | `legendre(n, x)` | Legendre polynomial P_n(x). | | `lgamma(x)` | ln\|Γ(x)\|. | | `log(x)` | function | | `log10(x)` | function | | `log2(x)` | function | | `nan` | constant | | `pi` | constant | | `round(x, digits=0)` | function | | `sin(x)` | function | | `sinh(x)` | function | | `sqrt(x)` | function | | `tan(x)` | function | | `tanh(x)` | function | | `tau` | constant | ### `use mechanics` — 22 names | Name | What it is | |---|---| | `beam(length, E, I, support='cantilever', loads=None, distributed=0.0, n=100)` | Euler–Bernoulli beam by finite elements (cubic Hermite elements, exact at the nodes for these loads). | | `hamiltonian_flow(H, q0, p0, dt, steps, method='leapfrog', every=1)` | Hamilton's equations q' = ∂H/∂p, p' = −∂H/∂q with gradients from automatic differentiation. "leapfrog" is symplectic for separable H = T(p) + V(q); "rk4" works for any H. Returns record(t, q, p, energy). | | `hohmann(r1, r2, mu)` | Hohmann transfer between circular orbits: record(dv1, dv2, dv_total, time). | | `inertia_tensor(masses, positions, about='com')` | Inertia tensor of point masses about their centre of mass (about="com") or the origin ("origin"). | | `kepler_solve(M, e)` | Eccentric anomaly E from the mean anomaly M (Kepler's equation M = E − e sin E), 0 ≤ e < 1. | | `lagrangian_flow(L, q0, qdot0, dt, steps, every=1)` | Euler–Lagrange equations of L(q, qdot), derived by automatic differentiation: M(q, q̇) q̈ = ∂L/∂q − (∂²L/∂q̇∂q) q̇ with M = ∂²L/∂q̇². Integrated with RK4. Returns record(t, q, qdot, energy) where energy = q̇·∂L/∂q̇ − L. | | `nbody(masses, x0, v0, dt, steps, G=6.6743e-11, softening=0.0, method='yoshida4', every=1)` | Gravitational N-body integration (direct summation, symplectic). Positions/velocities have shape [N, 3] (or [N, 2]). Returns record(t, x, v, energy, momentum, angular_momentum, energy_drift). | | `normal_modes(K, M)` | Normal modes of M x'' + K x = 0: angular frequencies ω (rad/s), frequencies (Hz) and mass-normalized mode shapes (columns), from the generalized symmetric eigenproblem K φ = ω² M φ. | | `orbit_elements(r, v, mu)` | Classical orbital elements from a state vector: a, e, i, raan, argp, nu (angles in radians). | | `orbit_state(a, e, i, raan, argp, nu, mu)` | Position and velocity from classical orbital elements -> record(r, v). | | `orbital_period(a, mu)` | T = 2π √(a³/μ) in seconds. | | `principal_axes(I)` | Principal moments (ascending) and axes (columns) of an inertia tensor. | | `quat(axis, angle)` | Unit quaternion [w, x, y, z] for a rotation by `angle` about `axis`. | | `quat_conj(q)` | function | | `quat_mul(a, b)` | Hamilton product a·b. | | `quat_rotate(q, v)` | Rotate vector v by unit quaternion q. | | `quat_to_matrix(q)` | function | | `rigid_body(I, omega0, q0, dt, steps, torque=None, every=1)` | Free (or torqued) rotation of a rigid body: Euler's equations in the body frame + quaternion attitude, RK4. I: 3×3 body inertia tensor (or its 3 principal moments); torque(t, omega, q) in the body frame. Returns record(t | | `rotation_matrix(axis, angle)` | Rotation matrix for a right-handed rotation by `angle` about `axis` (Rodrigues). | | `simulate(accel, x0, v0, dt, steps, method='verlet', t0=0.0, every=1)` | Integrate x'' = accel(x, v, t) (accel may also take (x) or (x, v)). | | `truss2d(nodes, members, E, A, supports, loads)` | Plane pin-jointed truss by the direct stiffness method. nodes: [[x, y], ...]; members: [[i, j], ...] (0-based node indices); E, A: numbers or one per member; supports: [[node, fix_x, fix_y], ...] with true/false; loads: | | `vis_viva(r, a, mu)` | Orbital speed at distance r on an orbit of semi-major axis a. | ### `use medical` — 32 names | Name | What it is | |---|---| | `anion_gap(sodium, chloride, bicarbonate, potassium=None)` | Serum anion gap (mmol/L): Na − (Cl + HCO3) [+ K if given]. | | `bmi(weight, height)` | Body-mass index in kg/m². weight in kg, height in m (numbers) or as quantities (70kg, 175cm). | | `brier_score(prob, outcome)` | Mean squared error of predicted probabilities against 0/1 outcomes. | | `bsa(weight, height, method='mosteller')` | Body-surface area in m² (Mosteller or DuBois & DuBois). weight in kg, height in cm (numbers) or quantities. | | `cockcroft_gault(age, weight, creatinine, sex)` | Creatinine clearance (mL/min) by Cockcroft–Gault. weight in kg, creatinine in mg/dL. | | `cohen_kappa(a, b, level=0.95)` | Cohen's kappa for two raters' categorical labels, with its asymptotic standard error and CI. | | `concordance_index(time, risk, event, higher_is_worse=True)` | Harrell's C: the fraction of comparable pairs whose predicted risk orders the event times correctly (ties in prediction count ½). | | `cox_ph(time, event, X, names=None, level=0.95, max_iter=50)` | Cox proportional-hazards regression (Efron's method for ties), Newton–Raphson. Returns coefficients, SE, z, p, hazard ratios with CI, log-likelihood, likelihood-ratio test and the concordance index. | | `diagnostic_metrics(tp, fp, fn, tn, level=0.95)` | Accuracy of a binary test against a reference standard, with confidence intervals (Wilson for proportions, log method for likelihood ratios and the diagnostic odds ratio). | | `DISCLAIMER` | value | | `ecg_rpeaks(ecg, fs, min_rr=0.25)` | R-peak detection in a single-lead ECG (Pan–Tompkins style: 5–15 Hz band-pass, derivative, squaring, 150 ms integration, adaptive threshold, refinement to the true maximum). Returns record(indices, times, rr (s), heart_ra | | `egfr_ckd_epi_2021(creatinine, age, sex)` | eGFR (mL/min/1.73 m²) by the race-free CKD-EPI 2021 creatinine equation. creatinine in mg/dL. | | `final_size(R0)` | Final epidemic size z of the SIR model: z = 1 − exp(−R0·z) (fully susceptible population). | | `hrv(rr, frequency_domain=True)` | Heart-rate variability from RR intervals in seconds: mean RR, SDNN, RMSSD, SDSD, pNN50, mean HR (ms and bpm) and, with ≥ 60 s of data, LF (0.04–0.15 Hz), HF (0.15–0.4 Hz) power and LF/HF from a 4 Hz resampled tachogram ( | | `ideal_body_weight(height, sex)` | Devine ideal body weight (kg); height in cm (number) or as a quantity. | | `incidence_rate(cases, person_time, level=0.95, per=1.0)` | Incidence rate with the exact (Poisson / chi-squared) confidence interval, scaled per `per` person-time. | | `kaplan_meier(time, event, level=0.95)` | Kaplan–Meier estimate: record(times, at_risk, events, censored, survival, se (Greenwood), ci (log-log), median). event: 1 = event observed, 0 = censored. | | `logrank(time, event, group)` | Log-rank test comparing survival between two or more groups: record(statistic, p, df, observed, expected, groups). | | `mantel_haenszel(tables, level=0.95)` | Pooled odds ratio and risk ratio over strata of 2×2 tables [[a, b], [c, d]] (Mantel–Haenszel) with the Robins–Breslow–Greenland CI, the Cochran–Mantel–Haenszel test and the Breslow–Day-free homogeneity summary. | | `mean_arterial_pressure(systolic, diastolic)` | MAP ≈ DBP + (SBP − DBP)/3 (same unit as the inputs, usually mmHg). | | `nca(t, c, dose=None, route='extravascular', lambda_points=None)` | Non-compartmental analysis of one concentration–time profile (linear-up / log-down trapezoids). Returns Cmax, Tmax, AUC_last, AUC_inf, AUC_%extrap, AUMC, λz (with R² and points used), t½, MRT and, when dose is given, CL | | `pk_one_compartment(dose, V, t, CL=None, ke=None, route='iv', ka=None, F=1.0, tau=None, doses=1, infusion_time=None)` | Plasma concentration of a one-compartment model with first-order elimination. | | `pk_two_compartment(dose, V1, k10, k12, k21, t)` | IV bolus into a two-compartment model: C(t) = A e^{−αt} + B e^{−βt}. Returns record(c, alpha, beta, A, B, t_half_alpha, t_half_beta). | | `qtc(qt, rr=None, hr=None, method='fridericia')` | Heart-rate-corrected QT (ms). qt in ms; give rr (s) or hr (beats/min). | | `read_dicom(path, include_identifiers=False)` | Read an uncompressed DICOM image: record(pixels (rescaled, e.g. Hounsfield units), rows, columns, pixel_spacing (mm), slice_thickness, modality, … ). Patient-identifying fields are left out unless include_identifiers=tru | | `read_dicom_series(folder, include_identifiers=False)` | Read every DICOM slice in a folder and stack them into a volume sorted by position along the slice normal (or instance number): record(volume[z, y, x], spacing [z, y, x] mm, positions, info). | | `risk_measures(a, b, c, d, level=0.95)` | 2×2 table: exposed (a cases, b non-cases), unexposed (c cases, d non-cases). Relative risk (log CI), odds ratio (Woolf CI), risk difference (Wald CI), NNT and the attributable fraction. | | `roc(scores, labels, level=0.95)` | ROC analysis: record(fpr, tpr, thresholds, auc, auc_ci (DeLong, logit-free), auc_se, youden_threshold, youden_sensitivity, youden_specificity). labels: 1 = disease/positive, 0 = negative. | | `roc_compare(scores1, scores2, labels)` | Paired DeLong test of two AUCs measured on the same subjects. | | `seir(beta, sigma, gamma, S0, E0, I0, R0=0.0, days=200, dt=0.1)` | SEIR model with incubation rate sigma (1/latent period). Returns record(t, S, E, I, R, peak_day, R0). | | `sir(beta, gamma, S0, I0, R0=0.0, days=160, dt=0.1)` | SIR epidemic model (counts or fractions). Returns record(t, S, I, R, peak_day, peak_infected, R0, attack_rate). | | `window_level(image, center, width)` | Map intensities (e.g. Hounsfield units) to [0, 1] for display with a window centre and width (e.g. soft tissue 40/400, lung −600/1500, bone 400/1800, brain 40/80). | ### `use memory` — 13 names | Name | What it is | |---|---| | `array(shape, dtype='float32', fill=None, ram=None)` | An array that lives in RAM while it fits the budget, else in a memory-mapped file on the SSD. ram=true/false forces the choice. | | `best_device(need_gb=0.0)` | The GPU with the most free memory (if it has need_gb free), otherwise "cpu". | | `budget(gb=None)` | Set (or read) the RAM budget for array(), in GB. | | `cached(fn=None, name=None, ram_mb=128, disk_gb=5)` | Memoise fn in a two-tier store (RAM, then SSD). Use as @memory.cached or memory.cached(fn). | | `chunks(a, rows=65536)` | function | | `devices()` | function | | `fit_batch(fn, batch, minimum=1)` | fn(batch); on out-of-memory the batch is halved (GPU cache emptied) and tried again. | | `free(a)` | Release an array from array() now (RAM is returned, a spill file is deleted). | | `map(fn, a, rows=65536, out=None, workers=None)` | function | | `on_disk(a)` | function | | `spill_dir()` | function | | `status()` | function | | `store(name='default', ram_mb=256, disk_gb=10)` | function | ### `use models` — 16 names | Name | What it is | |---|---| | `dequantize_model(params)` | function | | `gguf_info(path)` | function | | `hf_pipeline(task, model=None, device=None, **kw)` | A Hugging Face transformers pipeline: classify = hf_pipeline("sentiment-analysis"). | | `load_gguf(path, nested=True)` | function | | `load_model(path, verify=True, key=None)` | function | | `load_safetensors(path, dtype=None, names=None, nested=True)` | function | | `model_info(path)` | function | | `model_size(params)` | Bytes used by all tensors in a parameter tree (counts compact formats at their real size). | | `onnx_model(path, providers=None)` | Run an ONNX model with onnxruntime: m = onnx_model("net.onnx"); m({"input": x}). | | `qlinear(x, W, b=None)` | x @ W (+ b) where W may be a QTensor — decodes, multiplies and accumulates in FP32. | | `quantize_model(params, dtype=None, min_size=1024)` | Store every weight matrix (>= min_size elements) in a compact format; others stay as-is. | | `safetensors_info(path)` | function | | `save_gguf(path, params, metadata=None, dtype='f16', name=None, architecture='apsi')` | function | | `save_model(path, params, metadata=None, shard='4GB', sign=False)` | function | | `save_safetensors(path, params, metadata=None)` | function | | `torch_load(path, map_location='cpu')` | function | ### `use net` — 2 names | Name | What it is | |---|---| | `http_get(url, timeout=30)` | function | | `http_post_json(url, data, timeout=30)` | function | ### `use nn` — 57 names | Name | What it is | |---|---| | `accuracy(logits, labels)` | function | | `adagrad(lr=0.01, eps=1e-10)` | function | | `adam(lr=0.001, b1=0.9, b2=0.999, eps=1e-08)` | function | | `adamw(lr=0.001, b1=0.9, b2=0.999, eps=1e-08, weight_decay=0.01)` | function | | `attention(q, k, v, mask=None)` | Scaled dot-product attention over the last two axes. | | `batch_norm(x, gamma=None, beta=None, eps=1e-05, axis=0)` | Batch normalisation over `axis` (training-mode statistics). | | `batches(X, y, size, seed=None)` | function | | `bce(prob, target)` | Binary cross-entropy on probabilities (clamped like PyTorch: log ≥ −100). | | `bce_with_logits(logits, target)` | Binary cross-entropy on logits (numerically stable): mean(softplus(z) − y·z). | | `causal_mask(T)` | function | | `clip_grad_norm(grads, max_norm=1.0)` | function | | `conv2d(x, w, b=None, stride=1, padding=0)` | function | | `conv_init(c_in, c_out, k=3, seed=None)` | function | | `cosine_lr(step, total, base=0.001, warmup=0, minimum=0.0)` | function | | `cross_entropy(logits, labels)` | Mean cross-entropy of integer labels; labels [B] with logits [B, C], or labels [B, T] with logits [B, T, C] (sequence models: every position counts). | | `dense(n_in, n_out, seed=None, scale=None)` | A dense layer's parameters: record(W, b) with He initialisation. | | `dropout(x, p=0.1, train=True, seed=None)` | function | | `embedding_init(vocab, d, seed=None)` | function | | `gelu(x, approximate='tanh')` | GELU. approximate="tanh" (default, fast) or "none" (exact: x·Φ(x), as PyTorch's default). | | `global_norm(grads)` | function | | `gru(p, xs)` | function | | `gru_cell(p, x, h)` | function | | `gru_init(n_in, n_hidden, seed=None)` | function | | `huber_loss(pred, target, delta=1.0)` | Huber / smooth-L1 loss (mean), quadratic below delta and linear above. | | `l1_loss(pred, target)` | Mean absolute error. | | `layer_norm(x, gamma=None, beta=None, eps=1e-05)` | function | | `linear_warmup(step, base=0.001, warmup=100)` | function | | `load_params(path, like)` | function | | `loader(X, y, batch_size=32, shuffle=False, seed=None)` | Iterable mini-batches (like a PyTorch DataLoader over a TensorDataset); reshuffled every epoch. | | `log_softmax(x, axis=-1)` | function | | `lstm(p, xs)` | Run an LSTM over a sequence xs: [B, T, n_in] -> outputs [B, T, n_hidden]. | | `lstm_cell(p, x, h, c)` | One LSTM step: returns (h, c). x: [B, n_in], h, c: [B, n_hidden]. | | `lstm_init(n_in, n_hidden, seed=None)` | LSTM parameters: one fused weight for the 4 gates (input, forget, cell, output). | | `make_blobs(n=300, centers=3, dim=2, spread=0.5, seed=0)` | function | | `make_moons(n=200, noise=0.1, seed=0)` | function | | `make_regression(n=100, features=3, noise=0.1, seed=0)` | function | | `max_pool2d(x, k=2)` | function | | `mlp_init(sizes, seed=0)` | function | | `mse(pred, target)` | function | | `nll_loss(log_probs, labels)` | Negative log-likelihood of integer labels given log-probabilities [N, C]. | | `norm_init(d)` | function | | `numeric_grad(f, x, eps=1e-06)` | Finite-difference gradient (for checking autodiff). | | `one_hot(labels, k)` | function | | `relu(x)` | function | | `rms_norm(x, gamma=None, eps=1e-06)` | function | | `rmsprop(lr=0.001, decay=0.9, eps=1e-08)` | function | | `save_params(path, params)` | function | | `set_lr(opt, lr)` | function | | `sgd(lr=0.01, momentum=0.0)` | function | | `sigmoid(x)` | function | | `softmax(x, axis=-1)` | function | | `spiral_data(n_per_class=100, classes=3, noise=0.2, seed=0)` | Classic spiral toy dataset -> (X: Tensor[N, 2], y: Tensor[N] of class ids). | | `standardize(X, axis=0)` | Return (X_scaled, mean, std) so new data can be scaled the same way. | | `step_lr(step, base=0.001, every=100, factor=0.5)` | function | | `tanh(x)` | function | | `train(loss_fn, params, data, opt, epochs=10, batch_size=None, seed=0, verbose=True, clip=None, schedule=None, …)` | Generic training loop. | | `train_test_split(X, y, test=0.2, seed=0)` | function | ### `use parallel` — 12 names | Name | What it is | |---|---| | `atomic(value=0)` | function | | `bench(f, repeat=5, warmup=1)` | Time f() -> record(best, mean, runs) in seconds (as Time quantities). | | `channel(capacity=0)` | function | | `cpu_count()` | function | | `lock()` | function | | `parallel_for(items, f, workers=None)` | Call f(item) for every item on a thread pool; returns when all are done. | | `parallel_reduce(items, f, combine, init, workers=None)` | Map f over items in parallel, then fold the results with combine(acc, x). | | `set_threads(n)` | Limit compute threads (BLAS and Apsi's worker pool). Returns the active setting. | | `sleep(t)` | function | | `spawn(f, *args, **kwargs)` | Run f(*args) concurrently; returns a Task (use task.wait() for the result). | | `wait_all(tasks, timeout=None)` | function | | `wait_any(tasks, timeout=None)` | Return (index, result) of the first task to finish. | ### `use pde` — 9 names | Name | What it is | |---|---| | `advection1d(u0, c, dx, dt, steps, scheme='lax-wendroff', every=1)` | u_t + c u_x = 0 on a periodic grid ("upwind": first order, monotone; "lax-wendroff": second order). | | `bicgstab(A, b, tol=1e-10, max_iter=None, x0=None)` | BiCGSTAB for general square A. Returns record(x, iterations, residual, converged). | | `cg(A, b, tol=1e-10, max_iter=None, x0=None)` | Conjugate gradients for symmetric positive-definite A. Returns record(x, iterations, residual, converged). | | `heat1d(u0, dx, dt, steps, alpha, bc='dirichlet', left=None, right=None, every=1)` | u_t = α u_xx by Crank–Nicolson. bc: "dirichlet" (end values fixed at left/right, default: the initial end values), "neumann" (zero flux) or "periodic". Returns record(t, x, u) with u[frame, i]. | | `heat2d(u0, dx, dt, steps, alpha, every=1)` | u_t = α ∇²u on a square grid by the ADI method (unconditionally stable, second order). Boundary values stay at their initial values. Returns record(t, u) with u[frame, i, j]. | | `navier_stokes2d(omega0, nu, length=6.283185307179586, dt=0.001, steps=1000, every=0)` | 2-D incompressible Navier–Stokes in vorticity form ω_t + u·∇ω = ν∇²ω on a periodic square, pseudo-spectral with 2/3 de-aliasing and RK4 (viscous term integrated exactly). Returns record(t, omega, u, v, energy) for the st | | `poisson2d(f, dx, bc='dirichlet')` | Solve ∇²u = f on a uniform grid with the 5-point Laplacian. "dirichlet": u = 0 on the boundary (f gives the interior points); "periodic": f must have zero mean, the solution has zero mean. Fast transforms: O(N log N), ex | | `tridiag_solve(a, b, c, d)` | Solve tridiagonal systems: a sub-diagonal (a[0] unused), b diagonal, c super-diagonal (c[-1] unused). d may have extra trailing dimensions (several right-hand sides solved at once). | | `wave1d(u0, v0, c, dx, dt, steps, bc='fixed', every=1)` | u_tt = c² u_xx by the leapfrog scheme (needs c·dt ≤ dx). bc: "fixed", "free" or "periodic". Returns record(t, x, u). | ### `use physics` — 60 names | Name | What it is | |---|---| | `a_0` | constant | | `activity(N, half_life)` | Activity λN in becquerel. | | `alpha_fs` | constant | | `AU` | constant | | `biot_savart(path, current, points)` | Magnetic field (T) of a current along a polyline `path` [[x, y, z], ...] at the given points. Each straight segment uses the exact finite-wire formula. | | `blackbody_power(T, area=1.0, emissivity=1.0)` | Radiated power ε σ A T⁴. | | `brewster_angle(n1, n2)` | function | | `c` | constant | | `carnot_efficiency(T_hot, T_cold)` | function | | `coulomb_field(charges, positions, points, softening=0.0)` | Electric field (V/m) of point charges at the given points (2-D or 3-D), shape [points, dim]. | | `coulomb_potential(charges, positions, points, softening=0.0)` | function | | `critical_angle(n1, n2)` | function | | `cyclotron_frequency(q, B, m)` | Angular cyclotron frequency \|q\|B/m (rad/s). | | `de_broglie(m, v)` | Matter wavelength h/(m v) (non-relativistic). | | `decay(N0, half_life, t)` | Remaining amount N0 · 2^(−t/T½) (same unit as N0). | | `doppler(f, v_source=0.0, v_observer=0.0, c_wave=343.0)` | Classical Doppler shift; speeds positive when source and observer approach each other. | | `double_slit(theta, spacing, width, wavelength)` | Relative intensity of two slits of finite width. | | `epsilon_0` | constant | | `fresnel(n1, n2, theta)` | Fresnel power coefficients for s and p polarization -> [Rs, Rp, Ts, Tp]. | | `G` | constant | | `g0` | constant | | `gradient(u, dx, axis=0)` | Central-difference derivative of a field along one axis (periodic). | | `h` | constant | | `half_life_from_rate(lam)` | function | | `hbar` | constant | | `heat_conduction(k, area, dT, thickness)` | Fourier's law: heat flow k A ΔT / d (W). | | `ideal_gas_pressure(n, T, V)` | p = n R T / V (n in mol). | | `k_B` | constant | | `laplacian(u, dx, boundary='periodic')` | Discrete Laplacian (∇²u) on a regular grid with spacing dx. | | `length_contraction(L, v)` | function | | `lorentz_boost(t, x, v)` | Coordinates (t', x') in a frame moving with speed v along x -> [t', x']. | | `lorentz_factor(v)` | γ = 1/√(1 − v²/c²). | | `lorentz_force(q, E, B, v)` | F = q (E + v × B). | | `m_e` | constant | | `M_earth` | constant | | `m_n` | constant | | `m_p` | constant | | `M_sun` | constant | | `maxwell_speed_pdf(v, m, T)` | Maxwell–Boltzmann speed distribution f(v) (s/m). | | `mean_speed(m, T)` | function | | `meshgrid(x, y)` | function | | `mu_0` | constant | | `N_A` | constant | | `photon_energy(wavelength)` | function | | `planck(wavelength, T)` | Spectral radiance B_λ(λ, T) in W/(m²·sr·m). | | `q_e` | constant | | `R_earth` | constant | | `R_gas` | constant | | `relativistic_doppler(f, v)` | Frequency received from a source receding at speed v (negative v: approaching). | | `relativistic_energy(m, v)` | Total energy γ m c². | | `relativistic_momentum(m, v)` | function | | `rms_speed(m, T)` | function | | `sigma_SB` | constant | | `single_slit(theta, width, wavelength)` | Relative Fraunhofer intensity of a single slit: sinc²(π a sinθ / λ). | | `skin_depth(f, sigma, mu_r=1.0)` | Electromagnetic skin depth √(2/(ω μ σ)) of a good conductor. | | `snell(n1, n2, theta)` | Refraction angle (radians); NaN for total internal reflection. | | `thin_lens(f, d_o)` | Image distance and magnification for a thin lens -> [d_i, m]. | | `time_dilation(dt, v)` | Dilated time γ·Δt of a moving clock's proper interval Δt. | | `velocity_add(u, v)` | Relativistic velocity addition (u + v)/(1 + uv/c²). | | `wien_peak(T)` | Wavelength of maximum spectral radiance (Wien's law). | ### `use physics.constants` — 21 names | Name | What it is | |---|---| | `a_0` | constant | | `alpha_fs` | constant | | `AU` | constant | | `c` | constant | | `epsilon_0` | constant | | `G` | constant | | `g0` | constant | | `h` | constant | | `hbar` | constant | | `k_B` | constant | | `m_e` | constant | | `M_earth` | constant | | `m_n` | constant | | `m_p` | constant | | `M_sun` | constant | | `mu_0` | constant | | `N_A` | constant | | `q_e` | constant | | `R_earth` | constant | | `R_gas` | constant | | `sigma_SB` | constant | ### `use physics.fields` — 3 names | Name | What it is | |---|---| | `gradient(u, dx, axis=0)` | Central-difference derivative of a field along one axis (periodic). | | `laplacian(u, dx, boundary='periodic')` | Discrete Laplacian (∇²u) on a regular grid with spacing dx. | | `meshgrid(x, y)` | function | ### `use physics.formulas` — 36 names | Name | What it is | |---|---| | `activity(N, half_life)` | Activity λN in becquerel. | | `biot_savart(path, current, points)` | Magnetic field (T) of a current along a polyline `path` [[x, y, z], ...] at the given points. Each straight segment uses the exact finite-wire formula. | | `blackbody_power(T, area=1.0, emissivity=1.0)` | Radiated power ε σ A T⁴. | | `brewster_angle(n1, n2)` | function | | `carnot_efficiency(T_hot, T_cold)` | function | | `coulomb_field(charges, positions, points, softening=0.0)` | Electric field (V/m) of point charges at the given points (2-D or 3-D), shape [points, dim]. | | `coulomb_potential(charges, positions, points, softening=0.0)` | function | | `critical_angle(n1, n2)` | function | | `cyclotron_frequency(q, B, m)` | Angular cyclotron frequency \|q\|B/m (rad/s). | | `de_broglie(m, v)` | Matter wavelength h/(m v) (non-relativistic). | | `decay(N0, half_life, t)` | Remaining amount N0 · 2^(−t/T½) (same unit as N0). | | `doppler(f, v_source=0.0, v_observer=0.0, c_wave=343.0)` | Classical Doppler shift; speeds positive when source and observer approach each other. | | `double_slit(theta, spacing, width, wavelength)` | Relative intensity of two slits of finite width. | | `fresnel(n1, n2, theta)` | Fresnel power coefficients for s and p polarization -> [Rs, Rp, Ts, Tp]. | | `half_life_from_rate(lam)` | function | | `heat_conduction(k, area, dT, thickness)` | Fourier's law: heat flow k A ΔT / d (W). | | `ideal_gas_pressure(n, T, V)` | p = n R T / V (n in mol). | | `length_contraction(L, v)` | function | | `lorentz_boost(t, x, v)` | Coordinates (t', x') in a frame moving with speed v along x -> [t', x']. | | `lorentz_factor(v)` | γ = 1/√(1 − v²/c²). | | `lorentz_force(q, E, B, v)` | F = q (E + v × B). | | `maxwell_speed_pdf(v, m, T)` | Maxwell–Boltzmann speed distribution f(v) (s/m). | | `mean_speed(m, T)` | function | | `photon_energy(wavelength)` | function | | `planck(wavelength, T)` | Spectral radiance B_λ(λ, T) in W/(m²·sr·m). | | `relativistic_doppler(f, v)` | Frequency received from a source receding at speed v (negative v: approaching). | | `relativistic_energy(m, v)` | Total energy γ m c². | | `relativistic_momentum(m, v)` | function | | `rms_speed(m, T)` | function | | `single_slit(theta, width, wavelength)` | Relative Fraunhofer intensity of a single slit: sinc²(π a sinθ / λ). | | `skin_depth(f, sigma, mu_r=1.0)` | Electromagnetic skin depth √(2/(ω μ σ)) of a good conductor. | | `snell(n1, n2, theta)` | Refraction angle (radians); NaN for total internal reflection. | | `thin_lens(f, d_o)` | Image distance and magnification for a thin lens -> [d_i, m]. | | `time_dilation(dt, v)` | Dilated time γ·Δt of a moving clock's proper interval Δt. | | `velocity_add(u, v)` | Relativistic velocity addition (u + v)/(1 + uv/c²). | | `wien_peak(T)` | Wavelength of maximum spectral radiance (Wien's law). | ### `use physics.units` — 0 names | Name | What it is | |---|---| ### `use precision` — 28 names | Name | What it is | |---|---| | `bf16` | value | | `bf8` | value | | `dequantize(q)` | function | | `e4m3` | value | | `e5m2` | value | | `f16` | value | | `f32` | value | | `f64` | value | | `format_info(fmt)` | function | | `fp16` | value | | `fp32` | value | | `fp4` | value | | `fp4_e2m1` | value | | `fp6` | value | | `fp64` | value | | `fp6_e2m3` | value | | `fp6_e3m2` | value | | `fp8` | value | | `fp8_e4m3` | value | | `fp8_e5m2` | value | | `int4` | value | | `int8` | value | | `nbytes(x)` | function | | `qmatmul(a, b)` | Matrix multiply with quantised operands: decode to FP32, multiply, accumulate in FP32 (the same numerics as low-precision tensor-core GEMMs). | | `quant_error(x, fmt, axis=None)` | Relative RMS error introduced by storing x in `fmt`. | | `quantize(x, fmt, scale='auto', axis=None)` | Quantise a tensor. scale="auto" maps max\|x\| to the format's largest value (per tensor, or per channel along `axis`); scale=1.0 keeps raw values (no scaling). | | `round_to(x, fmt, scale='auto', axis=None)` | Round x to what the format can represent and return it as a normal tensor ("fake quantisation"). Gradients pass straight through, so this works inside training. | | `uint8` | value | ### `use pycompat` — 146 names | Name | What it is | |---|---| | `abs(x, /)` | Return the absolute value of the argument. | | `aiter(async_iterable, /)` | Return an AsyncIterator for an AsyncIterable object. | | `all(iterable, /)` | Return True if bool(x) is True for all values x in the iterable. | | `anext(…)` | Return the next item from the async iterator. | | `any(iterable, /)` | Return True if bool(x) is True for any x in the iterable. | | `ArithmeticError(/, *args, **kwargs)` | Base class for arithmetic errors. | | `ascii(obj, /)` | Return an ASCII-only representation of an object. | | `AssertionError(/, *args, **kwargs)` | Assertion failed. | | `AttributeError(/, *args, **kwargs)` | Attribute not found. | | `BaseException(/, *args, **kwargs)` | Common base class for all exceptions | | `BaseExceptionGroup(/, *args, **kwargs)` | A combination of multiple unrelated exceptions. | | `bin(number, /)` | Return the binary representation of an integer. | | `BlockingIOError(/, *args, **kwargs)` | I/O operation would block. | | `bool(/, *args, **kwargs)` | Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed. | | `breakpoint(*a, **k)` | function | | `BrokenPipeError(/, *args, **kwargs)` | Broken pipe. | | `BufferError(/, *args, **kwargs)` | Buffer error. | | `bytearray(/, *args, **kwargs)` | bytearray(iterable_of_ints) -> bytearray bytearray(string, encoding[, errors]) -> bytearray bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer bytearray(int) -> bytes array of size given by the parameter initi | | `bytes(/, *args, **kwargs)` | bytes(iterable_of_ints) -> bytes bytes(string, encoding[, errors]) -> bytes bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer bytes(int) -> bytes object of size given by the parameter initialized with null byte | | `BytesWarning(/, *args, **kwargs)` | Base class for warnings about bytes and buffer related problems, mostly related to conversion from str or comparing to str. | | `callable(obj, /)` | Return whether the object is callable (i.e., some kind of function). | | `ChildProcessError(/, *args, **kwargs)` | Child process error. | | `chr(i, /)` | Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff. | | `classmethod(/, *args, **kwargs)` | Convert a function to be a class method. | | `compile(*a, **k)` | function | | `complex(/, *args, **kwargs)` | Create a complex number from a string or numbers. | | `ConnectionAbortedError(/, *args, **kwargs)` | Connection aborted. | | `ConnectionError(/, *args, **kwargs)` | Connection error. | | `ConnectionRefusedError(/, *args, **kwargs)` | Connection refused. | | `ConnectionResetError(/, *args, **kwargs)` | Connection reset. | | `delattr(obj, name)` | function | | `DeprecationWarning(/, *args, **kwargs)` | Base class for warnings about deprecated features. | | `dict(/, *args, **kwargs)` | dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**k | | `dir(*obj)` | function | | `divmod(x, y, /)` | Return the tuple (x//y, x%y). Invariant: div*y + mod == x. | | `Ellipsis` | value | | `EncodingWarning(/, *args, **kwargs)` | Base class for warnings about encodings. | | `enumerate(/, *args, **kwargs)` | Return an enumerate object. | | `EnvironmentError(/, *args, **kwargs)` | Base class for I/O related errors. | | `EOFError(/, *args, **kwargs)` | Read beyond end of file. | | `eval(*a, **k)` | function | | `Exception(/, *args, **kwargs)` | Common base class for all non-exit exceptions. | | `ExceptionGroup(/, *args, **kwargs)` | type | | `exec(*a, **k)` | function | | `exit(code=None)` | function | | `FileExistsError(/, *args, **kwargs)` | File already exists. | | `FileNotFoundError(/, *args, **kwargs)` | File not found. | | `filter(/, *args, **kwargs)` | Return an iterator yielding those items of iterable for which function(item) is true. If function is None, return the items that are true. | | `float(/, *args, **kwargs)` | Convert a string or number to a floating-point number, if possible. | | `FloatingPointError(/, *args, **kwargs)` | Floating-point operation failed. | | `format(value, format_spec='', /)` | Return type(value).__format__(value, format_spec) | | `frozenset(/, *args, **kwargs)` | Build an immutable unordered collection of unique elements. | | `FutureWarning(/, *args, **kwargs)` | Base class for warnings about constructs that will change semantically in the future. | | `GeneratorExit(/, *args, **kwargs)` | Request that a generator exit. | | `getattr(obj, name, *default)` | function | | `globals(*a, **k)` | function | | `hasattr(obj, name)` | function | | `hash(obj, /)` | Return the hash value for the given object. | | `hex(number, /)` | Return the hexadecimal representation of an integer. | | `id(obj, /)` | Return the identity of an object. | | `ImportError(/, *args, **kwargs)` | Import can't find module, or can't find name in module. | | `ImportWarning(/, *args, **kwargs)` | Base class for warnings about probable mistakes in module imports | | `IndentationError(/, *args, **kwargs)` | Improper indentation. | | `IndexError(/, *args, **kwargs)` | Sequence index out of range. | | `input(prompt='')` | function | | `int(/, *args, **kwargs)` | int([x]) -> integer int(x, base=10) -> integer | | `InterruptedError(/, *args, **kwargs)` | Interrupted by signal. | | `IOError(/, *args, **kwargs)` | Base class for I/O related errors. | | `IsADirectoryError(/, *args, **kwargs)` | Operation doesn't work on directories. | | `isinstance(obj, class_or_tuple, /)` | Return whether an object is an instance of a class or of a subclass thereof. | | `issubclass(cls, class_or_tuple, /)` | Return whether 'cls' is derived from another class or is the same class. | | `iter(…)` | iter(iterable) -> iterator iter(callable, sentinel) -> iterator | | `KeyboardInterrupt(/, *args, **kwargs)` | Program interrupted by user. | | `KeyError(/, *args, **kwargs)` | Mapping key not found. | | `len(obj, /)` | Return the number of items in a container. | | `list(/, *args, **kwargs)` | Built-in mutable sequence. | | `locals(*a, **k)` | function | | `LookupError(/, *args, **kwargs)` | Base class for lookup errors. | | `map(/, *args, **kwargs)` | Make an iterator that computes the function using arguments from each of the iterables. Stops when the shortest iterable is exhausted. | | `max(…)` | max(iterable, *[, default=obj, key=func]) -> value max(arg1, arg2, *args, *[, key=func]) -> value | | `MemoryError(/, *args, **kwargs)` | Out of memory. | | `memoryview(/, *args, **kwargs)` | Create a new memoryview object which references the given object. | | `min(…)` | min(iterable, *[, default=obj, key=func]) -> value min(arg1, arg2, *args, *[, key=func]) -> value | | `ModuleNotFoundError(/, *args, **kwargs)` | Module not found. | | `NameError(/, *args, **kwargs)` | Name not found globally. | | `next(…)` | next(iterator[, default]) | | `NotADirectoryError(/, *args, **kwargs)` | Operation only works on directories. | | `NotImplemented` | value | | `NotImplementedError(/, *args, **kwargs)` | Method or function hasn't been implemented yet. | | `object(/, *args, **kwargs)` | The base class of the class hierarchy. | | `oct(number, /)` | Return the octal representation of an integer. | | `open(file, mode='r', *args, **kwargs)` | function | | `ord(character, /)` | Return the ordinal value of a character. | | `OSError(/, *args, **kwargs)` | Base class for I/O related errors. | | `OverflowError(/, *args, **kwargs)` | Result too large to be represented. | | `PendingDeprecationWarning(/, *args, **kwargs)` | Base class for warnings about features which will be deprecated in the future. | | `PermissionError(/, *args, **kwargs)` | Not enough permissions. | | `pow(base, exp, mod=None)` | Equivalent to base**exp with 2 arguments or base**exp % mod with 3 arguments | | `print(*args, sep=' ', end='\n', file=None, flush=False)` | Prints the values to a stream, or to sys.stdout by default. | | `ProcessLookupError(/, *args, **kwargs)` | Process not found. | | `property(/, *args, **kwargs)` | Property attribute. | | `pyfrom(module, name)` | `from module import name`: an attribute of the module, or its submodule. | | `pyimport(name)` | pyimport() for Python-exact files: pure standard modules (and their submodules) need no permission; with --allow-python=all every module is the real module object. | | `pystar(module, *names)` | `from module import *`: the public names, in one go. | | `PythonFinalizationError(/, *args, **kwargs)` | Operation blocked during Python finalization. | | `quit(code=None)` | function | | `range(/, *args, **kwargs)` | range(stop) -> range object range(start, stop[, step]) -> range object | | `RecursionError(/, *args, **kwargs)` | Recursion limit exceeded. | | `ReferenceError(/, *args, **kwargs)` | Weak ref proxy used after referent went away. | | `repr(obj, /)` | Return the canonical string representation of the object. | | `ResourceWarning(/, *args, **kwargs)` | Base class for warnings about resource usage. | | `reversed(/, *args, **kwargs)` | Return a reverse iterator over the values of the given sequence. | | `round(number, ndigits=None)` | Round a number to a given precision in decimal digits. | | `RuntimeError(/, *args, **kwargs)` | Unspecified run-time error. | | `RuntimeWarning(/, *args, **kwargs)` | Base class for warnings about dubious runtime behavior. | | `set(/, *args, **kwargs)` | Build an unordered collection of unique elements. | | `setattr(obj, name, value)` | function | | `slice(/, *args, **kwargs)` | slice(stop) slice(start, stop[, step]) | | `sorted(iterable, /, *, key=None, reverse=False)` | Return a new list containing all items from the iterable in ascending order. | | `staticmethod(/, *args, **kwargs)` | Convert a function to be a static method. | | `StopAsyncIteration(/, *args, **kwargs)` | Signal the end from iterator.__anext__(). | | `StopIteration(/, *args, **kwargs)` | Signal the end from iterator.__next__(). | | `str(/, *args, **kwargs)` | str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str | | `sum(iterable, /, start=0)` | Return the sum of a 'start' value (default: 0) plus an iterable of numbers | | `super(/, *args, **kwargs)` | super() -> same as super(__class__, ) super(type) -> unbound super object super(type, obj) -> bound super object; requires isinstance(obj, type) super(type, type2) -> bound super object; requires issubcla | | `SyntaxError(/, *args, **kwargs)` | Invalid syntax. | | `SyntaxWarning(/, *args, **kwargs)` | Base class for warnings about dubious syntax. | | `SystemError(/, *args, **kwargs)` | Internal error in the Python interpreter. | | `SystemExit(/, *args, **kwargs)` | Request to exit from the interpreter. | | `TabError(/, *args, **kwargs)` | Improper mixture of spaces and tabs. | | `TimeoutError(/, *args, **kwargs)` | Timeout expired. | | `tuple(/, *args, **kwargs)` | Built-in immutable sequence. | | `type(/, *args, **kwargs)` | type | | `TypeError(/, *args, **kwargs)` | Inappropriate argument type. | | `UnboundLocalError(/, *args, **kwargs)` | Local name referenced but not bound to a value. | | `UnicodeDecodeError(/, *args, **kwargs)` | Unicode decoding error. | | `UnicodeEncodeError(/, *args, **kwargs)` | Unicode encoding error. | | `UnicodeError(/, *args, **kwargs)` | Unicode related error. | | `UnicodeTranslateError(/, *args, **kwargs)` | Unicode translation error. | | `UnicodeWarning(/, *args, **kwargs)` | Base class for warnings about Unicode related problems, mostly related to conversion problems. | | `UserWarning(/, *args, **kwargs)` | Base class for warnings generated by user code. | | `ValueError(/, *args, **kwargs)` | Inappropriate argument value (of correct type). | | `vars(*obj)` | function | | `Warning(/, *args, **kwargs)` | Base class for warning categories. | | `ZeroDivisionError(/, *args, **kwargs)` | Second argument to a division or modulo operation was zero. | | `zip(/, *args, **kwargs)` | The zip object yields n-length tuples, where n is the number of iterables passed as positional arguments to zip(). The i-th element in every tuple comes from the i-th iterable argument to zip(). This continues until the | ### `use python` — 5 names | Name | What it is | |---|---| | `from_py(x)` | Python/JSON value -> Apsi value ({value, unit} dicts become Quantities). | | `from_torch(t)` | function | | `pyimport(name)` | function | | `to_py(x)` | Apsi value -> plain Python/JSON-friendly value (units become {value, unit}). | | `to_torch(x, device=None)` | function | ### `use quantum` — 38 names | Name | What it is | |---|---| | `anticommutator(A, B)` | function | | `apply(state, gate, targets)` | Apply a k-qubit gate to the given target qubits (list, in the gate's qubit order) of an n-qubit state. | | `bell(k=0)` | Bell states: 0 Φ+, 1 Φ−, 2 Ψ+, 3 Ψ−. | | `bloch(state)` | Bloch vector [x, y, z] of a single-qubit state or density matrix. | | `bra(bits)` | function | | `commutator(A, B)` | function | | `concurrence(rho)` | Wootters concurrence of a two-qubit state (0 = separable, 1 = maximally entangled). | | `controlled(gate)` | Controlled version of a gate (control = first qubit). | | `dagger(A)` | function | | `density(state)` | Density matrix \|ψ⟩⟨ψ\|. | | `entropy(rho, base=2)` | Von Neumann entropy −Tr ρ log ρ (bits by default). | | `evolve1d(psi0, V, x, dt, steps, mass=9.1093837015e-31, hbar=1.054571817e-34, every=1)` | Time evolution with the second-order split-operator method (periodic box, norm-preserving). Returns record(t, psi[frame, i], norm, x_mean). | | `expect_p(psi, x, hbar=1.054571817e-34)` | ⟨p⟩ = ∫ ψ* (−iħ ∂ψ/∂x) dx (spectral derivative). | | `expect_x(psi, x)` | function | | `expm(A)` | Matrix exponential (scaling and squaring with a Padé(13) approximant). | | `expm_hermitian(H, t=1.0, hbar=1.0)` | exp(−i H t / ħ) for Hermitian H (exact, by diagonalization). | | `fidelity(a, b)` | Fidelity between two pure states \|⟨a\|b⟩\|², or two density matrices (Uhlmann). | | `gates` | A lightweight named-field container: record(W=..., b=...). | | `gaussian_packet(x, x0, sigma, k0=0.0)` | Normalized Gaussian wave packet centred at x0 with width sigma and mean wavenumber k0. | | `ghz(n)` | GHZ state (\|0…0⟩ + \|1…1⟩)/√2. | | `hydrogen_energy(n, Z=1)` | Bohr energy levels −13.605693 eV · Z²/n² (infinite nuclear mass). | | `hydrogen_radial(n, l, r)` | Normalized radial function R_nl(r) of hydrogen, r in Bohr radii (∫ R² r² dr = 1). | | `ket(bits)` | Computational basis state, e.g. ket("010") or ket([0, 1, 0]). | | `kron(*mats)` | Tensor (Kronecker) product of any number of matrices or vectors. | | `measure(state, shots=1000, seed=None)` | Sample measurement outcomes: record of bitstring -> count. | | `partial_trace(rho, keep, n=None)` | Reduced density matrix of the qubits in `keep` (list of indices). | | `pauli()` | Pauli matrices: record(I, X, Y, Z). | | `phase(phi)` | function | | `probabilities(state)` | Measurement probabilities in the computational basis. | | `probability(psi, x, a, b)` | ∫_a^b \|ψ\|² dx. | | `purity(rho)` | function | | `rx(theta)` | function | | `ry(theta)` | function | | `rz(theta)` | function | | `schrodinger1d(V, x, mass=9.1093837015e-31, n=5, hbar=1.054571817e-34)` | Lowest n bound states of −ħ²/(2m) ψ'' + V ψ = E ψ on the grid x (ψ = 0 beyond the ends; fourth-order finite differences). V: function of x or values on the grid. Returns record(energies, energies_ev, psi[n, len(x)], x). | | `spin_ops(s=0.5, hbar=1.0)` | Spin operators for spin s (in units of ħ by default): record(x, y, z, plus, minus, squared). | | `time_evolution(H, psi0, times, hbar=1.0)` | ψ(t) = exp(−iHt/ħ) ψ0 for every t in `times` -> array [len(times), dim]. | | `uncertainty(psi, x, hbar=1.054571817e-34)` | record(dx, dp, product) — standard deviations of position and momentum. | ### `use random` — 5 names | Name | What it is | |---|---| | `permutation(n, seed=None)` | function | | `rand(*shape, seed=None)` | function | | `randint(lo, hi, *shape, seed=None)` | function | | `randn(*shape, seed=None)` | function | | `seed(n)` | function | ### `use rl` — 25 names | Name | What it is | |---|---| | `bandit(arms=10, n=1, seed=None)` | function | | `beam_search(step, start, beam_width=4, max_len=20, eos=None)` | Beam search: step(sequence) returns log-probabilities over the next token (1-D). Returns the best sequences as a list of record(tokens, score), best first. | | `best_of_n(candidates, score)` | The candidate with the highest score(candidate), with all scores. | | `cartpole(n=1, seed=None, max_steps=500)` | function | | `categorical_entropy(logits)` | function | | `categorical_log_prob(logits, actions)` | log π(a) for integer actions (differentiable in logits). | | `categorical_sample(logits, seed=None, rng=None)` | Sample actions from softmax(logits) (Gumbel-max). | | `discounted_returns(rewards, dones, gamma=0.99, last_value=None)` | Discounted returns of time-major rewards [T] or [T, N]; done[t] ends the episode after step t. | | `dpo_loss(policy_chosen, policy_rejected, ref_chosen, ref_rejected, beta=0.1, label_smoothing=0.0)` | Direct Preference Optimization. Inputs are summed log-probabilities of the chosen / rejected responses under the policy and the frozen reference model. Returns record(loss, chosen_rewards, rejected_rewards, accuracy). | | `dqn(env, model, params, total_steps=50000, buffer_size=50000, batch_size=64, lr=0.001, gamma=0.99, learning_…)` | Deep Q-learning: replay buffer, ε-greedy exploration, target network, (double) DQN target, Huber loss. model(params, obs) returns Q-values [batch, actions]. Returns record(params, episode_returns). | | `evaluate(env, policy, episodes=10, seed=0)` | Run policy(obs) -> actions until `episodes` episodes have finished; returns their mean and all returns. | | `gae(rewards, values, dones, last_values, gamma=0.99, lam=0.95)` | Generalized Advantage Estimation (time-major [T, N]). Returns record(advantages, returns). | | `gaussian_entropy(log_std)` | function | | `gaussian_log_prob(mean, log_std, actions)` | Sum over action dimensions of log N(a; mean, exp(log_std)²) (differentiable). | | `gridworld(size=5, n=1, seed=None)` | function | | `grpo_advantages(rewards, group_size, eps=1e-06)` | GRPO: rewards [B·G] of G completions per prompt -> (r − group mean) / (group std + eps). | | `ipo_loss(policy_chosen, policy_rejected, ref_chosen, ref_rejected, tau=0.1)` | Identity Preference Optimization: mean((h − 1/(2τ))²) with h the log-ratio difference. | | `kl_penalized_rewards(rewards, logp, ref_logp, beta=0.05)` | Per-token rewards for RLHF with PPO: −β·(log π − log π_ref) at every token plus the sequence reward on the last token. logp / ref_logp: [B, T]; rewards: [B]. Returns [B, T]. | | `mcts(state, legal_actions, apply, is_terminal, reward, simulations=1000, c=1.41, rollout_depth=200, seed=0, t…)` | Monte-Carlo tree search (UCT). Functions: legal_actions(s) -> list, apply(s, a) -> new state, is_terminal(s) -> bool, reward(s) -> value from the point of view of the player who moved last when two_player=true (otherwise | | `ppo(env, model, params, total_steps=100000, rollout=128, epochs=4, minibatch=256, lr=0.0003, gamma=0.99, lam…)` | Proximal Policy Optimization (clipped surrogate, GAE, value and entropy terms, gradient clipping). `model(params, obs)` returns [logits, value]. Returns record(params, episode_returns, updates). | | `ppo_clip_loss(logp, old_logp, advantages, clip=0.2, mask=None)` | Clipped PPO surrogate for token log-probs [B, T] (mask: 1 for response tokens). | | `preference_loss(r_chosen, r_rejected, margin=0.0)` | Bradley–Terry reward-model loss: −log σ(r_chosen − r_rejected − margin), averaged. | | `reinforce(env, model, params, episodes=500, lr=0.01, gamma=0.99, seed=0, max_steps=1000)` | REINFORCE with a running-mean baseline on a single environment (n=1). model(params, obs) -> logits. | | `sample_token(logits, temperature=1.0, top_k=None, top_p=None, seed=None)` | Sample from logits [..., V] with temperature, top-k and nucleus (top-p) filtering; temperature 0 = greedy. | | `sequence_log_prob(logits, tokens, mask=None)` | Σ_t log softmax(logits[b, t])[tokens[b, t]] for each sequence (logits [B, T, V], tokens [B, T]). | ### `use rt` — 20 names | Name | What it is | |---|---| | `backend(name=None)` | rt.backend() → the current frame backend; rt.backend("cpu" \| "gpu" \| "auto"). "auto" picks the GPU only for frames large enough to beat the copy to the card (≥ 1600×900); "gpu" without a usable card stays on the CPU and | | `box_blur(img, r=1)` | Mean filter of radius r (cost independent of r). | | `camera(index=0, width=None, height=None, fps=None, fourcc=None)` | A camera through OpenCV (pip install opencv-python); needs --allow-python. `width` may be a size name or "WxH" ("fhd", "1600x900"). | | `diff(a, b, threshold=0.0)` | \|a - b\| per pixel (largest channel for colour); with a threshold, a boolean mask of pixels that changed more than it. | | `downsample(img, k=2)` | Average k×k blocks (fast, no aliasing from skipping pixels). | | `edges(img, low=50, high=150)` | Edge map (Canny with OpenCV; gradient magnitude above `high` otherwise), uint8 0/255. | | `ema(prev, new, alpha=0.2)` | Exponential moving average (smooths noise while staying responsive). | | `fps_meter()` | function | | `gray(rgb)` | Luminance of an RGB(A) image (BT.601). uint8 in → uint8 out (fast path); other types → float32. | | `has_opencv` | OpenCV, imported on first use (it takes ~20 ms to load); falsy when it is not installed. | | `loop(hz, step, seconds=None, steps=None, catch_up=False, on_error='raise')` | Call step(tick, t) `hz` times per second for `seconds` (or `steps` ticks). A tick that cannot start on time is counted as missed and skipped (catch_up=true runs it late instead), so the rhythm never drifts. on_error="ski | | `motion(prev, cur, threshold=25.0, step=None)` | Did something move? record(moved, fraction, cx, cy, box): changed share of pixels, the centre of the change and its bounding box [x0, y0, x1, y1] in full-frame pixels. Works on gray or colour frames of any size; above 64 | | `resize(img, width, height=None)` | Resize to width×height (area averaging when shrinking — no aliasing; smooth when enlarging). resize(img, 0.5) scales by a factor. | | `scheduler()` | function | | `size(width, height=None)` | (width, height) from 1600, 900 / "1600x900" / "fhd" / "1080p" / (w, h). | | `sizes` | value | | `synthetic(width=640, height=None, fps=120.0, noise=False)` | function | | `threshold(img, t, high=255)` | Pixels above t become `high`, the rest 0 (uint8). | | `ticks(hz, seconds=None, steps=None)` | A for-loop that runs at `hz`: `for tick in rt.ticks(1000, seconds=2):`. Yields the tick number. | | `video(source, process, fps=None, seconds=None, frames=None, adapt=False, min_scale=0.25, on_error='skip', gray…)` | Run `process(frame, info)` on the newest frame from `source` until `seconds` or `frames` is reached. | ### `use scale` — 4 names | Name | What it is | |---|---| | `gpu_table()` | function | | `inference(params, gpu='H100', precision='bf16', context=8192, batch=1, kv_heads=None, layers=None, width=None, act…)` | Memory to serve a model: weights + KV cache, and the GPUs needed. | | `number(x)` | '21B' -> 2.1e10, '1T' -> 1e12, '15T' tokens, or a plain number. | | `plan(params, tokens=None, gpu='H100', gpus=None, days=None, active=None, layers=None, width=None, heads=None,…)` | Plan a training run. Returns a record with the layout, memory per GPU, GPU count and duration. | ### `use sci` — 20 names | Name | What it is | |---|---| | `bisect(f, a, b, tol=1e-12, max_iter=200)` | function | | `conj(x)` | function | | `cumtrapz(y, x)` | function | | `curve_fit(model, x, y, p0, sigma=None, max_iter=200, tol=1e-12)` | Levenberg–Marquardt least squares with an exact autodiff Jacobian. | | `einsum(spec, *operands)` | Einstein summation, e.g. einsum("bij,bjk->bik", A, B). Units multiply; differentiable. | | `fft(x, axis=-1)` | function | | `fftfreq(n, d=1.0)` | function | | `find_root(f, x0, tol=1e-12, max_iter=100)` | Newton's method with the exact derivative from autodiff (unit-aware). | | `ifft(x, axis=-1)` | function | | `imag(x)` | function | | `integrate(f, a, b, points=64)` | ∫_a^b f(x) dx by Gauss–Legendre quadrature. f receives a tensor of points. Exact for polynomials up to degree 2·points−1; units are [f]·[x]. | | `interp(x, xp, fp)` | Piecewise-linear interpolation (units of xp and x must match). | | `irfft(x, n=None, axis=-1)` | function | | `minimize(f, x0, method='lbfgs', steps=500, tol=1e-10, lr=0.01, history=10, verbose=False)` | Minimise a scalar function of a parameter tree using exact gradients. | | `power_spectrum(x, d=1.0)` | One-sided power spectrum \|rfft\|² with matching frequencies -> (freqs, power). | | `real(x)` | function | | `rfft(x, axis=-1)` | function | | `rfftfreq(n, d=1.0)` | function | | `solve_ivp(f, t_span, y0, method='bdf', rtol=1e-06, atol=1e-09, t_eval=None, first_step=None, max_steps=200000)` | Solve y' = f(t, y) from t_span[0] to t_span[1]. | | `trapz(y, x=None, dx=None)` | Trapezoidal integral of samples y over x (or uniform spacing dx). Units: [y]·[x]. | ### `use signal` — 24 names | Name | What it is | |---|---| | `bandpass(x, low, high, fs, order=4)` | Zero-phase Butterworth band-pass between low and high. | | `bandstop(x, low, high, fs, order=4)` | Zero-phase Butterworth band-stop. | | `blackman(n, sym=True)` | function | | `butter(order, cutoff, kind='lowpass', fs=2.0)` | Butterworth filter as second-order sections. cutoff in Hz (with fs) — a [low, high] pair for band filters. Default fs=2 means cutoff is a fraction of the Nyquist frequency, as in SciPy. | | `convolve(a, b, mode='full')` | Linear convolution ("full", "same" or "valid"), FFT-based for long inputs. | | `correlate(a, b, mode='full')` | Cross-correlation (as numpy.correlate / scipy.signal.correlate for real data). | | `detrend(x, kind='linear')` | Remove the mean ("constant") or the least-squares line ("linear"). | | `envelope(x)` | Amplitude envelope \|analytic signal\|. | | `filtfilt(sos, x, padlen=None)` | Zero-phase filtering: forward and backward, odd-extended edges, steady-state start (SciPy compatible). | | `find_peaks(x, height=None, distance=None, prominence=None)` | Local maxima (flat tops: the middle sample), filtered by minimum height, minimum distance in samples (taller peaks win) and minimum prominence — the same rules as scipy.signal.find_peaks. | | `freqz(sos, n=512, fs=6.283185307179586)` | Frequency response: record(f, h (complex), magnitude, magnitude_db, phase). | | `hamming(n, sym=True)` | function | | `hann(n, sym=True)` | Hann window. | | `highpass(x, cutoff, fs, order=4)` | Zero-phase Butterworth high-pass. | | `hilbert(x)` | Analytic signal x + i·H[x] (FFT method). | | `kaiser(n, beta=8.6)` | Kaiser window. | | `lowpass(x, cutoff, fs, order=4)` | Zero-phase Butterworth low-pass. | | `moving_average(x, n)` | Centered moving average over n samples (edges use the available samples). | | `notch(x, freq, fs, q=30.0)` | Remove one frequency (e.g. 50/60 Hz mains) with a second-order IIR notch, zero phase. | | `resample(x, num)` | Change the number of samples with the Fourier method (periodic signals; as scipy.signal.resample). | | `savgol(x, window, order)` | Savitzky–Golay smoothing (least-squares polynomial of `order` over `window` samples; polynomial fit at the edges, like SciPy's mode="interp"). | | `sosfilt(sos, x)` | Causal filtering with second-order sections (starts from rest). | | `spectrogram(x, fs=1.0, nperseg=256, noverlap=None)` | Short-time power spectrum (Hann window, density scaling): record(f, t, power[f, t]). | | `welch(x, fs=1.0, nperseg=256, noverlap=None, detrend='constant')` | Power spectral density by Welch's method (Hann window, one-sided, density scaling). | ### `use special` — 31 names | Name | What it is | |---|---| | `assoc_legendre(l, m, x)` | Associated Legendre function P_l^m(x) with the Condon–Shortley phase (as in physics texts). | | `besseli(n, x)` | Modified Bessel function of the first kind I_n(x), integer n. | | `besselj(n, x)` | Bessel function of the first kind J_n(x), integer n. | | `beta(a, b)` | B(a, b) = Γ(a)Γ(b)/Γ(a+b). | | `betainc(a, b, x)` | Regularized incomplete beta I_x(a, b). | | `betaincinv(a, b, p)` | x with I_x(a, b) = p. | | `chebyshev(n, x)` | Chebyshev polynomial of the first kind T_n(x). | | `comb(n, k)` | Binomial coefficient C(n, k) (exact for integers). | | `digamma(x)` | ψ(x) = Γ'(x)/Γ(x). | | `ellipe(m)` | Complete elliptic integral of the second kind E(m), m = k² ≤ 1 (AGM). | | `ellipk(m)` | Complete elliptic integral of the first kind K(m), m = k² < 1 (AGM). | | `erf(x)` | function | | `erfc(x)` | function | | `erfcinv(y)` | Inverse complementary error function. | | `erfinv(y)` | Inverse error function. | | `expint(x)` | Exponential integral E1(x) = ∫_x^∞ e^{-t}/t dt, x > 0. | | `factorial(n)` | n! (exact integer for integers). | | `gamma(x)` | Γ(x). | | `gammainc(a, x)` | Regularized lower incomplete gamma P(a, x). | | `gammaincc(a, x)` | Regularized upper incomplete gamma Q(a, x) = 1 − P(a, x). | | `gammaincinv(a, p)` | x with P(a, x) = p. | | `hermite(n, x)` | Physicists' Hermite polynomial H_n(x) (H_1 = 2x). | | `hermite_e(n, x)` | Probabilists' Hermite polynomial He_n(x) (He_1 = x). | | `laguerre(n, x, alpha=0.0)` | Generalized Laguerre polynomial L_n^(α)(x). | | `lbeta(a, b)` | function | | `legendre(n, x)` | Legendre polynomial P_n(x). | | `lgamma(x)` | ln\|Γ(x)\|. | | `perm(n, k)` | Number of ordered selections n!/(n-k)! (exact for integers). | | `sinc(x)` | sin(πx)/(πx) (normalized). | | `sph_harm(l, m, theta, phi)` | Spherical harmonic Y_l^m(θ, φ) (θ polar, φ azimuth), orthonormal, Condon–Shortley phase. Complex. | | `zeta(s)` | Riemann zeta ζ(s) for real s > 1 (Euler–Maclaurin). | ### `use stats` — 44 names | Name | What it is | |---|---| | `adjust_pvalues(p, method='holm')` | Adjusted p-values: "bonferroni", "holm", "bh" (Benjamini–Hochberg FDR) or "by" (Benjamini–Yekutieli). | | `anova_oneway(*groups)` | One-way ANOVA: F, p, degrees of freedom, η² and ω². | | `beta_dist(a, b)` | Beta distribution on [0, 1]. | | `binom_test(k, n, p=0.5, alternative='two-sided')` | Exact binomial test. | | `binomial(n, p)` | Binomial distribution: successes in n trials with probability p. | | `bootstrap_ci(data, statistic=None, n=10000, level=0.95, seed=0)` | Percentile bootstrap confidence interval of statistic(data) (default: the mean). | | `chi2(df)` | Chi-squared distribution. | | `chi2_contingency(table, correction=True)` | Chi-squared test of independence for an r×c table (Yates' correction for 2×2 by default). | | `chi2_gof(observed, expected=None, ddof=0)` | Chi-squared goodness-of-fit test (expected counts default to uniform). | | `cohen_d(a, b)` | Cohen's d with the pooled standard deviation. | | `corr_matrix(X)` | Pearson correlation matrix of the columns of X. | | `cov_matrix(X)` | Sample covariance matrix (n−1) of the columns of X. | | `describe(x)` | n, mean, std (n−1), sem, min, quartiles, max, skewness, excess kurtosis and a 95 % CI of the mean. | | `exponential(rate=1.0)` | Exponential distribution with the given rate λ. | | `f_dist(d1, d2)` | F distribution (Fisher–Snedecor). | | `fisher_exact(table, alternative='two-sided')` | Fisher's exact test for a 2×2 table. Returns the sample odds ratio and the exact p-value. | | `gamma_dist(shape, scale=1.0)` | Gamma distribution (shape k, scale θ). | | `hedges_g(a, b)` | Hedges' g (small-sample corrected d). | | `jarque_bera(x)` | Jarque–Bera normality test. | | `kendalltau(x, y)` | Kendall's τ-b with the asymptotic p-value (ties handled). | | `kruskal(*groups)` | Kruskal–Wallis H test (tie-corrected). | | `ks_2samp(a, b, method='auto')` | Two-sample Kolmogorov–Smirnov test. method "exact" (default when m·n ≤ 10 000), or "asymptotic". | | `logit(X, y, add_intercept=True, names=None, level=0.95, max_iter=100)` | Logistic regression by Newton–Raphson (IRLS): coefficients, SE, z, p, CI, odds ratios with CI, log-likelihood, AIC and McFadden's pseudo-R². | | `lognormal(mu=0.0, sigma=1.0)` | Log-normal: log(X) ~ N(mu, sigma²). | | `mannwhitneyu(a, b, alternative='two-sided', continuity=True)` | Mann–Whitney U test (normal approximation with tie correction). U is for sample a. | | `mean_ci(x, level=0.95)` | Confidence interval of the mean (t distribution). | | `normal(mu=0.0, sigma=1.0)` | Normal distribution N(mu, sigma²). | | `ols(X, y, add_intercept=True, names=None, level=0.95)` | Ordinary least squares: coefficients with standard errors, t, p and confidence intervals; R², adjusted R², F test, residuals and a predict(X) function. | | `pearsonr(x, y, alternative='two-sided', level=0.95)` | Pearson correlation with p-value (t test) and Fisher-z confidence interval. | | `permutation_test(a, b, statistic=None, n=10000, seed=0, alternative='two-sided')` | Two-sample permutation test of statistic(a, b) (default: difference of means). | | `poisson(rate)` | Poisson distribution with mean `rate`. | | `power_ttest(effect, n, alpha=0.05, alternative='two-sided', kind='two-sample')` | Power of a t-test for a standardized effect size (Cohen's d), n per group (exact noncentral t). | | `proportion_ci(k, n, level=0.95, method='wilson')` | Confidence interval of a proportion k/n: "wilson" (default) or "clopper-pearson" (exact). | | `quantile(x, q)` | Quantiles (linear interpolation, as numpy/R type 7). | | `sample_size_ttest(effect, power=0.8, alpha=0.05, alternative='two-sided', kind='two-sample')` | Smallest n (per group for two-sample) that reaches the requested power. | | `sem(x)` | function | | `spearmanr(x, y, alternative='two-sided')` | Spearman rank correlation (t approximation for the p-value, as SciPy). | | `student_t(df, loc=0.0, scale=1.0)` | Student's t distribution with df degrees of freedom. | | `ttest_1samp(x, mu=0.0, alternative='two-sided', level=0.95)` | One-sample t-test of mean(x) = mu. | | `ttest_ind(a, b, equal_var=False, alternative='two-sided', level=0.95)` | Two-sample t-test (Welch by default; equal_var=true for Student's). | | `ttest_rel(a, b, alternative='two-sided', level=0.95)` | Paired t-test. | | `uniform(low=0.0, high=1.0)` | Continuous uniform distribution on [low, high]. | | `wilcoxon(x, y=None, alternative='two-sided', continuity=False)` | Wilcoxon signed-rank test (normal approximation, zeros dropped, tie correction). | | `zscore(x)` | function | ### `use sys` — 8 names | Name | What it is | |---|---| | `cpu_threads()` | function | | `device()` | "cuda:0" when a GPU meeting Apsi's target tier is usable through CuPy, otherwise "cpu". | | `gpu_available()` | function | | `gpu_info()` | GPU 0: name, compute capability, memory. Works with CuPy, or with just the NVIDIA driver. | | `hardware()` | function | | `to_cpu(x)` | function | | `to_device(x, dev=None)` | function | | `to_gpu(x)` | function | ### `use tokens` — 12 names | Name | What it is | |---|---| | `best(value, model='gpt-4o')` | Try the transforms that apply and return (text, method) with the fewest tokens; unpack with restore(). | | `count(text, model='gpt-4o')` | Tokens in text: exact with tiktoken installed (o200k / cl100k), otherwise an estimate (±10–15 %). | | `dedupe(text, min_chars=24, min_count=2)` | Lines that repeat are written once in a header and referenced as §N where they occur. | | `diff(old, new)` | Only what changed, as line operations: `=N` keep N lines, `-N` drop N lines, `+text` add a line. | | `exact()` | True when count() is exact (tiktoken installed). | | `expand(text)` | function | | `pack(value)` | Compact, exactly reversible text for a JSON value. A list of records with the same keys becomes a table: `@table ["id","name"]` then one line per record (`1,"ann"`). Anything else is minified JSON. | | `pack_jsonl(text)` | JSON Lines → pack() of the records (exact for the parsed values). | | `patch(old, delta)` | function | | `restore(text, method)` | function | | `savings(before, after, model='gpt-4o')` | function | | `unpack(text)` | function | ### `use torch` — 138 names | Name | What it is | |---|---| | `Adagrad(params=None, lr=0.01, lr_decay=0.0, weight_decay=0.0, initial_accumulator_value=0.0, eps=1e-10, foreach=None, maximize=False, differentiable=False, fused=None)` | type | | `Adam(params=None, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.0, amsgrad=False, maximize=False, foreach=None, capturable=False, differentiable=False, fused=None, decoupled_weight_decay=False)` | type | | `Adamax(params=None, lr=0.002, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.0, **_)` | type | | `AdamW(params=None, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.01, amsgrad=False, maximize=False, foreach=None, capturable=False, differentiable=False, fused=None)` | type | | `AdaptiveAvgPool1d(output_size)` | function | | `AdaptiveAvgPool2d(output_size)` | function | | `AdaptiveMaxPool1d(output_size, return_indices=False)` | function | | `AdaptiveMaxPool2d(output_size, return_indices=False)` | function | | `AvgPool1d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True)` | function | | `AvgPool2d(kernel_size, stride=None, padding=0, ceil_mode=False, count_include_pad=True, divisor_override=None)` | function | | `BatchNorm1d(*a, **k)` | function | | `BatchNorm2d(*a, **k)` | function | | `BCELoss(*args, **kw)` | function | | `BCEWithLogitsLoss(*args, **kw)` | function | | `Bilinear(in1_features, in2_features, out_features, bias=True)` | type | | `binary_cross_entropy(input, target, weight=None, size_average=None, reduce=None, reduction='mean')` | function | | `binary_cross_entropy_with_logits(input, target, weight=None, size_average=None, reduce=None, reduction='mean', pos_weight=None)` | PyTorch's stable form: (1−y)·x + (1 + (pw−1)·y)·(log(1+e^(−\|x\|)) + max(−x, 0)). | | `CELU(*args, inplace=False, **kw)` | function | | `celu(x, alpha=1.0)` | function | | `ChainedScheduler(schedulers, optimizer=None)` | type | | `checkpoint(function, *args, use_reentrant=None, **kw)` | torch.utils.checkpoint.checkpoint(fn, *args): every differentiable value fn uses must be an argument. | | `checkpoint_layer(layer, params, *xs, train=None, **kw)` | Apply layer(params, *xs) with gradient checkpointing (torch.utils.checkpoint): activations inside the layer are not stored but recomputed in the backward pass, which trades compute for memory. | | `clip_grad_norm(grads, max_norm, norm_type=2.0, error_if_nonfinite=False)` | PyTorch's clip_grad_norm_: scale every gradient by max_norm / (total_norm + 1e-6) when that is below 1. | | `clip_grad_value(grads, clip_value)` | function | | `ConstantLR(optimizer, factor=0.3333333333333333, total_iters=5, last_epoch=-1)` | type | | `Conv1d(*a, **k)` | function | | `Conv2d(*a, **k)` | function | | `cosine_similarity(x1, x2, dim=1, eps=1e-08)` | function | | `CosineAnnealingLR(optimizer, T_max, eta_min=0.0, last_epoch=-1)` | type | | `CosineAnnealingWarmRestarts(optimizer, T_0, T_mult=1, eta_min=0.0, last_epoch=-1)` | type | | `cross_entropy(input, target, weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean', label_s…)` | F.cross_entropy with class-index or class-probability targets, weights, ignore_index and smoothing. | | `CrossEntropyLoss(*args, **kw)` | function | | `CyclicLR(optimizer, base_lr, max_lr, step_size_up=2000, step_size_down=None, mode='triangular', gamma=1.0, scale_fn=None, scale_mode='cycle', cycle_momentum=True, base_momentum=0.8, max_momentum=0.9, last_epoch=-1)` | type | | `DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, generator=None, prefetch_factor=None, persistent_workers=False, pin_memory_device='', seed=None)` | Mini-batches from a dataset (TensorDataset, or any object with __len__ / __getitem__), reshuffled every epoch when shuffle=true; batches are stacked like PyTorch's default collate. | | `Dropout(p=0.5, inplace=False)` | type | | `dropout(x, p=0.5, train=True, training=None, inplace=False)` | Inverted dropout (as F.dropout): zero with probability p and scale the rest by 1/(1-p) while training. | | `ELU(*args, inplace=False, **kw)` | function | | `elu(x, alpha=1.0)` | function | | `Embedding(num_embeddings, embedding_dim, padding_idx=None, max_norm=None, scale_grad_by_freq=False, sparse=False, _weight=None)` | type | | `embedding(idx, weight, padding_idx=None)` | function | | `ExponentialLR(optimizer, gamma, last_epoch=-1)` | type | | `Flatten(start_dim=1, end_dim=-1)` | type | | `flatten(x, start_dim=0, end_dim=-1)` | function | | `GELU(*args, inplace=False, **kw)` | function | | `gelu(x, approximate='none')` | function | | `generate_square_subsequent_mask(sz)` | Causal mask: −inf above the diagonal (as nn.Transformer.generate_square_subsequent_mask). | | `GLU(*args, inplace=False, **kw)` | function | | `glu(x, dim=-1)` | function | | `GroupNorm(num_groups, num_channels, eps=1e-05, affine=True, device=None, dtype=None)` | type | | `GRU(*a, **k)` | function | | `Hardsigmoid(*args, inplace=False, **kw)` | function | | `hardsigmoid(x)` | function | | `Hardswish(*args, inplace=False, **kw)` | function | | `hardswish(x)` | function | | `Hardtanh(*args, inplace=False, **kw)` | function | | `hardtanh(x, min_val=-1.0, max_val=1.0)` | function | | `huber_loss(input, target, reduction='mean', delta=1.0, weight=None)` | function | | `HuberLoss(*args, **kw)` | function | | `Identity(*args, inplace=False, **kw)` | function | | `identity(x)` | function | | `kl_div(input, target, size_average=None, reduce=None, reduction='mean', log_target=False)` | function | | `KLDivLoss(*args, **kw)` | function | | `l1_loss(input, target, size_average=None, reduce=None, reduction='mean')` | function | | `L1Loss(*args, **kw)` | function | | `LambdaLR(optimizer, lr_lambda, last_epoch=-1)` | type | | `layer_norm(x, normalized_shape, weight=None, bias=None, eps=1e-05)` | function | | `LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True, bias=True, device=None, dtype=None)` | type | | `leaky_relu(x, negative_slope=0.01)` | function | | `LeakyReLU(*args, inplace=False, **kw)` | function | | `Linear(in_features, out_features, bias=True, device=None, dtype=None)` | type | | `linear(x, weight, bias=None)` | function | | `LinearLR(optimizer, start_factor=0.3333333333333333, end_factor=1.0, total_iters=5, last_epoch=-1)` | type | | `load_state_dict(model, params, state, strict=True)` | Copy a PyTorch state_dict (or one saved by Apsi) into the parameter tree and the buffers of `model`. Missing or unexpected keys are errors when strict=true; shapes must match. | | `log_softmax(x, dim=None)` | function | | `LogSoftmax(*args, inplace=False, **kw)` | function | | `LSTM(*a, **k)` | function | | `MaxPool1d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)` | function | | `MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)` | function | | `Mish(*args, inplace=False, **kw)` | function | | `mish(x)` | function | | `Module(init, forward, name='model', parts=None)` | A model from its init(seed) and forward(p, x…, train) functions (models converted from PyTorch). | | `ModuleList(mods=())` | function | | `mse_loss(input, target, size_average=None, reduce=None, reduction='mean', weight=None)` | function | | `MSELoss(*args, **kw)` | function | | `MultiheadAttention(embed_dim, num_heads, dropout=0.0, bias=True, add_bias_kv=False, add_zero_attn=False, kdim=None, vdim=None, batch_first=False, device=None, dtype=None)` | type | | `MultiplicativeLR(optimizer, lr_lambda, last_epoch=-1)` | type | | `MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)` | type | | `NAdam(params=None, lr=0.002, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.0, momentum_decay=0.004, decoupled_weight_decay=False, **_)` | type | | `nll_loss(input, target, weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')` | function | | `NLLLoss(*args, **kw)` | function | | `normalize(x, p=2.0, dim=1, eps=1e-12)` | function | | `one_hot(t, num_classes=-1)` | function | | `OneCycleLR(optimizer, max_lr, total_steps=None, epochs=None, steps_per_epoch=None, pct_start=0.3, anneal_strategy='cos', cycle_momentum=True, base_momentum=0.85, max_momentum=0.95, div_factor=25.0, final_div_factor=10000.0, three_phase=False, last_epoch=-1)` | type | | `pad(x, pad, mode='constant', value=0.0)` | F.pad for constant padding: pad = (last_lo, last_hi, prev_lo, prev_hi, …). | | `parameters_count(params)` | function | | `PolynomialLR(optimizer, total_iters=5, power=1.0, last_epoch=-1)` | type | | `PReLU(num_parameters=1, init=0.25)` | type | | `RAdam(params=None, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.0, decoupled_weight_decay=False, **_)` | type | | `ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0.0, eps=1e-08)` | type | | `ReLU(*args, inplace=False, **kw)` | function | | `relu(x)` | function | | `ReLU6(*args, inplace=False, **kw)` | function | | `relu6(x)` | function | | `RMSNorm(normalized_shape, eps=None, elementwise_affine=True, device=None, dtype=None)` | type | | `RMSprop(params=None, lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0.0, momentum=0.0, centered=False, capturable=False, foreach=None, maximize=False, differentiable=False)` | type | | `RNN(*a, nonlinearity='tanh', **k)` | function | | `scaled_dot_product_attention(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, train=True)` | softmax(Q·Kᵀ/√d + mask)·V over the last two axes (PyTorch's F.scaled_dot_product_attention). | | `SELU(*args, inplace=False, **kw)` | function | | `selu(x)` | function | | `Sequential(*layers)` | type | | `SequentialLR(optimizer, schedulers, milestones, last_epoch=-1)` | type | | `SGD(params=None, lr=0.001, momentum=0.0, dampening=0.0, weight_decay=0.0, nesterov=False, maximize=False, foreach=None, differentiable=False, fused=None)` | type | | `Sigmoid(*args, inplace=False, **kw)` | function | | `sigmoid(x)` | function | | `SiLU(*args, inplace=False, **kw)` | function | | `silu(x)` | function | | `smooth_l1_loss(input, target, size_average=None, reduce=None, reduction='mean', beta=1.0)` | function | | `SmoothL1Loss(*args, **kw)` | function | | `Softmax(*args, inplace=False, **kw)` | function | | `softmax(x, dim=None)` | function | | `Softplus(*args, inplace=False, **kw)` | function | | `softplus(x, beta=1.0, threshold=20.0)` | function | | `Softsign(*args, inplace=False, **kw)` | function | | `softsign(x)` | function | | `state_dict(model, params)` | PyTorch-style state_dict of a model: parameters and buffers as one flat record of tensors ("encoder.layers.0.self_attn.in_proj_weight", "bn.running_mean", …). Saved with models.save_model it is a safetensors file PyTorch | | `StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)` | type | | `Tanh(*args, inplace=False, **kw)` | function | | `tanh(x)` | function | | `Tanhshrink(*args, inplace=False, **kw)` | function | | `tanhshrink(x)` | function | | `TensorDataset(*tensors)` | type | | `Transformer(d_model=512, nhead=8, num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=2048, dropout=0.1, activation='relu', custom_encoder=None, custom_decoder=None, layer_norm_eps=1e-05, batch_first=False, norm_first=False, bias=True, device=None, dtype=None)` | type | | `TransformerDecoder(decoder_layer, num_layers, norm=None)` | function | | `TransformerDecoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1, activation='relu', layer_norm_eps=1e-05, batch_first=False, norm_first=False, bias=True, device=None, dtype=None)` | type | | `TransformerEncoder(encoder_layer, num_layers, norm=None, enable_nested_tensor=True, mask_check=True)` | function | | `TransformerEncoderLayer(d_model, nhead, dim_feedforward=2048, dropout=0.1, activation='relu', layer_norm_eps=1e-05, batch_first=False, norm_first=False, bias=True, device=None, dtype=None)` | type | | `Unflatten(dim, unflattened_size)` | type | | `value_and_grad_aux(fn)` | value_and_grad for a function returning [loss, aux]: returns (loss, aux, grads). aux values (e.g. the logits used to compute the loss) are returned without their gradient graph, so nothing is computed twice. | ### `use web` — 10 names | Name | What it is | |---|---| | `abort(status, message='error')` | Abort the current request with an HTTP error: abort(404, "no such user"). | | `bearer_token(req)` | function | | `jwt_sign(claims, secret, expires_in=3600)` | Create an HS256 JSON Web Token. expires_in: seconds (number or Time quantity). | | `jwt_verify(token, secret, leeway=30)` | Return the claims of a valid token, or none if the signature/expiry/algorithm is wrong. | | `redirect(location, status=302)` | function | | `require_api_key(keys, header='x-api-key', exempt=())` | A before-hook accepting any of the given API keys (constant-time comparison). | | `require_jwt(secret, exempt=())` | A before-hook: rejects requests without a valid Bearer JWT; sets req.user to its claims. | | `response(body='', status=200, headers=None, type=None)` | Build a response explicitly: response("created", status=201, headers={"X-A": "1"}). | | `server(name='apsi', max_body=1048576, workers=None, max_connections=10000, log=True, handler_timeout=60.0, head…)` | function | | `websocket_connect(url, headers=None, timeout=10.0, subprotocols=None)` | function | ## Command line (43 commands) Every command takes `--help` (`apsi help run` works too). | Command | What it does | |---|---| | `apsi run` | run a .apsi program (default: the project's main file) | | `apsi debug` | debug a program in the terminal (breakpoints, stepping, variables) | | `apsi dap` | the Debug Adapter Protocol server (VS Code starts it for F5) | | `apsi native` | native compiler: info, explain FILE, ir FILE [FN], asm FILE [FN], clear-cache | | `apsi check` | statically check files or folders without running them | | `apsi test` | run test blocks in files or folders | | `apsi repl` | interactive prompt | | `apsi fmt` | format .apsi files in place (safe: syntax tree must stay identical) | | `apsi audit` | list the capabilities a program uses and the minimal flags it needs | | `apsi scan` | scan programs, folders and companion files for malware patterns | | `apsi verify` | check the installed runtime against its release manifest | | `apsi config` | show or change settings (security profile) | | `apsi lsp` | run the language server (stdio) for editors | | `apsi doctor` | check this machine against Apsi's hardware targets | | `apsi serve` | expose a program's functions as a JSON HTTP API | | `apsi ast` | print the syntax tree of a file | | `apsi tokens` | print the tokens of a file | | `apsi plan` | plan training of a large model: GPUs, layout, memory per GPU, days | | `apsi model` | inspect, verify and convert model files (GGUF, .apsim, safetensors) | | `apsi pkg` | install, list, remove and lock Python libraries (GPU, ONNX, PyTorch, …) | | `apsi install` | install Apsi libraries (.apsil) from the registry or a file | | `apsi uninstall` | remove installed Apsi libraries | | `apsi pack` | check a library and build dist/NAME-VERSION.apsil | | `apsi publish` | upload an .apsil library to the registry | | `apsi search` | search the library registry | | `apsi lib` | libraries: list, info, registry, login, logout | | `apsi yank` | hide a version of your library from new installs (--undo to show it again) | | `apsi unpublish` | delete a version of your library from the registry | | `apsi release` | upload, list or delete Apsi release files and benchmark results on the website | | `apsi editors` | install Apsi support into VS Code, Cursor, Windsurf, Sublime, Vim/Neovim, … | | `apsi convert` | translate Python files or folders into Apsi (numpy, math, torch, … mapped) | | `apsi version` | print the version | | `apsi new` | create a new project folder (apsi new NAME --template app\|lib\|physics\|ai\|server) | | `apsi init` | turn the current (or given) folder into an Apsi project | | `apsi task` | list or run the [tasks] of apsi.toml | | `apsi eval` | evaluate code from the command line and print the result | | `apsi watch` | re-run a program, tests or the checker whenever files change | | `apsi bench` | time a program over several runs (output suppressed) | | `apsi doc` | documentation for built-ins and modules (apsi doc nn, apsi doc sqrt) | | `apsi completion` | print a shell completion script (bash, zsh, fish, powershell) | | `apsi help` | show help for a command (apsi help run) | | `apsi build` | make an executable from a program (app.exe on Windows) | | `apsi compile` | parse, check and compile programs now so later runs start from the cache | # The Apsi Language Reference Apsi 2.4.4 · source files use the extension `.apsi` and the UTF-8 encoding. This document is the complete reference for Apsi: how programs are written, what every construct means, every built-in function and module, the interactive REPL and the `apsi` command line. Read it from the top to learn the language, or jump to a chapter. **Contents** 1. [A first look](#1-a-first-look) 2. [Running programs](#2-running-programs) 3. [Lexical structure](#3-lexical-structure) 4. [Values and types](#4-values-and-types) 5. [Variables and scope](#5-variables-and-scope) 6. [Expressions and operators](#6-expressions-and-operators) 7. [Statements and control flow](#7-statements-and-control-flow) 8. [Functions](#8-functions) 9. [Structs and records](#9-structs-and-records) 10. [Physical units](#10-physical-units) 11. [Measurement uncertainty](#11-measurement-uncertainty) 12. [Tensors and shapes](#12-tensors-and-shapes) 13. [Precision and number formats](#13-precision-and-number-formats) 14. [`evolve` — differential equations](#14-evolve--differential-equations) 15. [Automatic differentiation](#15-automatic-differentiation) 16. [Modules and imports](#16-modules-and-imports) 17. [Tests](#17-tests) 18. [The checker and errors](#18-the-checker-and-errors) 19. [Permissions and security](#19-permissions-and-security) 20. [Execution engines](#20-execution-engines) 21. [Built-in functions](#21-built-in-functions) 22. [Standard modules](#22-standard-modules) 23. [The REPL](#23-the-repl) 24. [The command line](#24-the-command-line) 25. [Projects and `apsi.toml`](#25-projects-and-apsitoml) 26. [Style guide](#26-style-guide) 27. [Grammar](#27-grammar) --- ## 1. A first look ```apsi # orbit.apsi — a few lines of everything use physics.constants # c, G, M_earth, R_earth, … struct Body: mass: Mass radius: Length = 1m fn surface_gravity(self) -> Acceleration = G * self.mass / self.radius ** 2 let earth = Body(M_earth, radius=R_earth) print(f"g = {earth.surface_gravity():.3f}") # g = 9.820 m/s^2 fn kinetic(m: Mass, v: Velocity) -> Energy = 0.5 * m * v ** 2 print(kinetic(2kg, 3[m/s])) # 9.0 J print(1[kWh] as [MJ]) # 3.6 MJ let squares = [i ** 2 for i in 0..10 if i % 2 == 0] print(squares |> sum) # 120 test "kinetic energy": assert kinetic(1kg, 2[m/s]) ~= 2J ``` What to notice: - blocks are made by **indentation**, as in Python; - numbers can carry **units** (`2kg`, `3[m/s]`), and units are checked *before* the program runs; - `let` declares a constant, `var` a variable; - `fn` defines functions, `struct` defines record types with fields and methods; - `test` blocks live next to the code and run with `apsi test`. --- ## 2. Running programs ```bash apsi hello.apsi # run a file (same as: apsi run hello.apsi) apsi run hello.apsi a b # program arguments, read with args() apsi check hello.apsi # check only, do not run apsi test # run test blocks (default: tests/ of the project) apsi # interactive REPL apsi -e "sqrt(2) * 3m" # evaluate one expression apsi new myproject # create a project folder ``` Every run first parses the whole file, then the **checker** reports every error it can prove (unit mismatches, undefined names, wrong arguments, …) without running a single line, then the program runs. Chapter 24 lists every command and flag. On Linux a file starting with `#!/usr/bin/env apsi` can be made executable with `chmod +x`. --- ## 3. Lexical structure ### 3.1 Encoding and lines - Source files are UTF-8. A byte-order mark (as written by Windows Notepad) and CRLF line endings are accepted. - A **comment** starts with `#` and runs to the end of the line. - Statements end at the end of a line. `;` separates several statements on one line: `let a = 1; let b = 2`. - Inside `()`, `[]` and `{}` lines may be broken freely. - Outside brackets, a line ending in `\` continues on the next line. - A line that **starts** with `|>` always continues the previous line, so pipelines can be written vertically: ```apsi let ds = from_tensors(X, y, batch=256) |> map(augment, workers=8) |> prefetch(4) ``` ### 3.2 Indentation A block follows a line ending in `:` and is indented further than that line. Use 4 spaces (a tab counts as 4). The block ends where the indentation returns to an outer level. A block of a single statement may stay on the same line: `if x < 0: return 0`. ### 3.3 Identifiers Names start with a letter or `_` and continue with letters, digits or `_`. Any Unicode letter is allowed: `let Δt = 1[ms]`, `let ω = 2 * pi * 50[Hz]`, `let größe = 3m`. Names starting with `_` are private to their file (not exported by `import`). Names are case-sensitive. ### 3.4 Keywords Reserved everywhere: ``` let var fn return if elif else while for in break continue struct true false none and or not use import assert evolve as pass ``` Contextual (special only in one place, ordinary names elsewhere): `try`, `except`, `finally`, `raise`, `yield` (statements, section 7.2–7.3), `class`, `with`, `del`, `global`, `nonlocal`, `async`, `await` (section 9.4 and 7.5–7.7), `is` (after an expression), `test` (starts a test block), `parallel` (only directly before `for`), `over step using tol record every into` (inside `evolve`). ### 3.5 Numbers | Form | Examples | Type | |---|---|---| | integer | `42`, `1_000_000` | `Int` | | decimal | `3.14`, `0.5`, `6.02e23`, `1e-9`, `2.5E+3` | `Float` | | with a unit (letters right after the digits, no space) | `5m`, `2kg`, `300K`, `10ms`, `3eV`, `5µs` | `Quantity` | | with a bracketed unit | `9.81[m/s^2]`, `8.314[J/(mol K)]`, `1[N*m]`, `5[m²]` | `Quantity` | `_` may separate digit groups. A number written directly before a name is always read as a unit: write `2 * x`, not `2x`. ### 3.6 Strings | Form | Example | |---|---| | plain | `"text"` | | multi-line | `"""line 1` ⏎ `line 2"""` | | formatted (f-string) | `f"x = {x:.3f}, list has {len(xs)} items"` | | formatted, multi-line | `f"""…{expr}…"""` | Escapes: `\n` (newline), `\t` (tab), `\r`, `\\`, `\"`, `\0`. Any other backslash is kept as it is, so Windows paths such as `"C:\data\x.csv"` mostly work, but `"C:\\data\\x.csv"` or `"C:/data/x.csv"` is clearer. In f-strings, `{expression}` inserts a value and `{expression:spec}` formats it with a Python-style format spec (`.3f`, `>8`, `,`, `e`, `%`, …). For quantities the spec formats the number and the unit is appended: `f"{9.81[m/s^2]:.1f}"` → `9.8 m/s^2`. `{{` and `}}` produce literal braces. ### 3.7 Operator characters `+ - * / // % ** ^ @ == != < <= > >= ~= = += -= *= /= //= %= **= @= -> => |> .. ..= . , : ' ( ) [ ] { }` Unicode alternatives: `×` and `·` for `*`, `−` for `-`, `≤ ≥ ≠ ≈` for `<= >= != ~=`, `→` for `->`, `±` (or `+/-`) for uncertainty. --- ## 4. Values and types | Type | Examples | Notes | |---|---|---| | `Int` | `42`, `-7`, `1_000` | arbitrary precision | | `Float` | `3.14`, `6.02e23`, `inf`, `nan` | IEEE 754 double | | `Quantity` (shown as its dimension, e.g. `Length`) | `2kg`, `9.81[m/s^2]`, `1.5[kWh]` | number + SI dimension, see chapter 10 | | `Exact` | `exact("1/3")`, values inside `@precision(exact)` | exact fractions | | `Uncertain` | `9.81[m/s^2] ± 0.02[m/s^2]`, `3.0 +/- 0.1` | see chapter 11 | | `Bool` | `true`, `false` | | | `Str` | `"text"`, `f"x = {x}"` | immutable Unicode text | | `None` | `none` | "no value" | | `List` | `[1, 2, 3]`, `[]` | mutable, any element types | | `Tuple` | `(1, 2)`, `(x,)`, `a, b` | immutable | | `Dict` | `{"k": 1}`, `{}` | keys are usually strings or numbers | | `Range` | `0..10` (10 excluded), `0..=10` (10 included) | lazy integer ranges, also with units: `0s..10s` | | `Tensor` | `tensor([[1, 2], [3, 4]])`, `zeros(3, 3)`, `randn(64, 128)` | n-dimensional arrays, may carry a unit | | `Record` | `record(W=w, b=b)` | ad-hoc container with named fields | | struct instance | `Particle(...)` | see chapter 9 | | `Fn` | `fn f(x) = …`, `fn(x) => x * 2`, built-ins | functions are values | `type(x)` returns the type name as a string (`"Int"`, `"Length"`, `"Tensor"`, …). **Truthiness.** In `if`, `while`, `and`, `or` and `not`: `false`, `none`, `0`, `0.0`, empty strings, lists, tuples and dicts are false; everything else is true. **Equality.** `==` compares values (lists element by element, quantities with their units). `a ~= b` compares approximately (relative tolerance 1e-6) and also checks units. ### 4.1 Strings Strings support `+` (concatenation), `*` with an `Int` (repetition), indexing `s[0]`, slicing `s[1:4]`, `len(s)`, `in` (substring test) and the usual methods: ```apsi let s = "Hello, World" s.upper() s.lower() s.strip() s.split(", ") s.replace("World", "Apsi") s.startswith("He") s.endswith("!") s.find("o") s.count("l") s.title() ", ".join(["a", "b"]) # or join(["a", "b"], ", ") chars(s) # list of characters str(42) + " items" # str() converts anything, int("42"), float("1.5") parse ``` ### 4.2 Lists, tuples and dicts ```apsi var xs = [3, 1, 2] xs.append(4) # also extend, insert, pop, remove, index, count, sort, reverse, clear, copy xs[0] xs[-1] xs[1:3] xs[::2] 2 in xs # membership [1, 2] + [3] # concatenation len(xs) sorted(xs) reversed(xs) sum(xs) min(xs) max(xs) let t = (1, "a") let a, b = t # unpacking var d = {"a": 1} d["b"] = 2 d.get("c", 0) d.keys() d.values() d.items() "a" in d keys(d) ``` ### 4.3 Comprehensions ```apsi [x * x for x in 0..10] [x for x in data if x > 0] [(i, j) for i in 0..3 for j in 0..3 if i != j] {k: v * 2 for k, v in d.items()} ``` --- ## 5. Variables and scope ```apsi let x = 10 # constant: cannot be assigned again var y = 0 # variable: can change let a, b = 1, 2 # several names at once (unpacks any sequence; one level, no nesting) let v: Velocity = 3[m/s] # optional type annotation, checked y += 1 # += -= *= /= //= %= **= @= xs[0] = 5 # element assignment (lists, dicts, tensors) p.mass = 2kg # field assignment (structs, records) ``` - A name must be declared with `let` or `var` before it is used — a typo is an error, not a new variable. - A `var` that holds a physical quantity **keeps its dimension**: assigning a `Time` to a `Length` variable is an error. - Blocks (`if`, `for`, `while`, function bodies) open a new scope. Names declared inside are not visible outside. - Loop variables and function parameters are constants. - Functions can read and modify (`var`) variables of the scopes around them (closures). - Declaring the same name again in an inner scope shadows the outer one. --- ## 6. Expressions and operators ### 6.1 Precedence (lowest first) | Level | Operators | Notes | |---|---|---| | lambda | `fn(x) => expr` | | | pipe | `x \|> f` | `x \|> f` is `f(x)`; `x \|> f(a)` is `f(x, a)` | | conditional | `a if cond else b` | | | none-coalescing | `a ?? b` | `a` unless it is `none`, then `b`; `b` runs only when needed | | walrus | `(n := expr)` | assigns and gives the value | | logical | `or`, `and`, `not` | short-circuit; `a or b` gives `a` or `b` itself | | comparison | `== != < <= > >= ~= in not in is is not` | chain: `0 < f(x) < 1` evaluates `f(x)` once | | bitwise or | `\|` | integers bit by bit, set union, dict merge, boolean tensors | | bitwise xor | `^` (only after `use pycompat`; elsewhere `^` is a power) | | | bitwise and | `&` | | | shifts | `<< >>` | | | range / conversion | `a..b`, `a..=b`, `x as [unit]` | | | additive | `+ -` | | | multiplicative | `* / // % @` | `@` is matrix multiplication | | unary | `-x`, `+x`, `~x` | `~` is bitwise not (logical not for boolean tensors) | | uncertainty | `x ± σ`, `x +/- σ` | binds tighter than `*` | | power | `**` or `^` | right-associative: `2**3**2 == 512`, `-2**2 == -4` | | postfix | `f(a, k=v)`, `x[i, j]`, `x[1:5:2]`, `x.field`, `x?.field`, `x?[i]` | `?.` / `?[`: `none` when `x` is `none` | ### 6.2 Arithmetic `/` always divides exactly (`7 / 2 == 3.5`); `//` is floor division (`-7 // 2 == -4`); `%` is the remainder with the sign of the divisor. Arithmetic on quantities follows the unit rules in chapter 10. Arithmetic on tensors is element-wise with NumPy broadcasting. ### 6.3 Pipes ```apsi data |> normalize |> train(model) # train(normalize(data), model) [1, 2, 3] |> sum # 6 ``` ### 6.3b Values that may be `none`: `??`, `?.`, `?[` ```apsi let port = config.get("port") ?? 8080 # 8080 when the key is missing (get gives none) let city = user?.address?.city ?? "unknown" # none anywhere on the way: "unknown" let top = results?[0] # none when results is none let name = find_user(id)?.name?.upper() # the whole chain stops at the first none ``` `a ?? b` is `a` unless `a` is `none` — only `none`: `0 ?? 1` is `0` and `"" ?? "x"` is `""` (use `or` for "empty or false"). `b` is evaluated only when it is needed and `a` exactly once, so `next_item() ?? default` calls `next_item` once. `x?.name`, `x?.f(args)` and `x?[i]` give `none` when `x` is `none` and otherwise act like `.` and `[ ]`; everything after them in the same chain is skipped as well, as in JavaScript, C# and Kotlin. Both work in the compiled engine, the tree-walking interpreter and the checker, and `apsi fmt` writes them as `a ?? b`, `a?.b`, `a?[i]`. ### 6.4 Indexing and slicing `x[i]`, `x[-1]` (from the end), `x[a:b]`, `x[a:b:step]`, `x[:, 0]` (tensor column), `x[i, j]` (tensor element), `x[mask]` (tensor boolean mask), `x[idx_tensor]` (gather). ### 6.5 Conditional expression ```apsi let label = "even" if n % 2 == 0 else "odd" ``` ### 6.6 Approximate comparison `a ~= b` is true when `|a - b| <= 1e-6 · max(|a|, |b|)` (and units match). For other tolerances use `approx(a, b, rel=1e-9, atol=1e-12)`. --- ## 7. Statements and control flow ```apsi if x > 0: print("positive") elif x == 0: print("zero") else: print("negative") while n > 1: n = n // 2 for i in 0..10: # 0, 1, …, 9 if i == 3: continue if i == 7: break print(i) for i, item in enumerate(items): # with an index pass # does nothing for k, v in d.items(): print(k, v) assert total > 0, "total must be positive" ``` - `for` iterates over ranges, lists, tuples, strings, dict keys, tensors (first axis), data pipelines and channels. - `break` leaves the innermost loop; `continue` starts its next iteration. - `pass` is an empty statement for blocks that intentionally do nothing. - `assert cond` stops with an `AssertionError` when `cond` is false. Without a message, the values of both sides of a comparison are shown: `assert x == 3` → `AssertionError: assertion failed: 4 == 3`. - `return` leaves a function (chapter 8). `return` outside a function is an error. - `error("message")` raises a `RuntimeError` with your message; `exit(code)` stops the program. ### 7.2 Errors: `try` / `except` / `else` / `finally` and `raise` ```apsi fn parse_speed(text): try: return float(text) * 1[m/s] except ValueError as e: # e.kind, e.message, e.line; str(e) is the message print(f"not a number: {e.message}") return none try: let cfg = load_json("config.json") except FileNotFoundError, PermissionError: # several kinds; (A, B) works too print("using defaults") except as e: # every other error (also: except Error as e) print("unexpected:", e.kind) raise # hand the same error on else: print("config loaded") # only when nothing failed finally: print("always runs") raise ValueError("speed must be positive") # the familiar Python kinds are built in raise "something went wrong" # a RuntimeError with this message ``` Kinds use Python's names, so converted code keeps its exact behaviour: `ValueError`, `TypeError`, `KeyError`, `IndexError`, `ZeroDivisionError`, `ArithmeticError`, `LookupError`, `AttributeError`, `NameError`, `RuntimeError`, `NotImplementedError`, `AssertionError`, `StopIteration`, `OSError`, `FileNotFoundError`, `PermissionError`, `TimeoutError`, `UnicodeError`, `RecursionError`, `OverflowError`, plus Apsi's `UnitError` and `ShapeError`. Errors Apsi raises itself are matched the same way: dividing by zero is a `ZeroDivisionError`, a missing dictionary key a `KeyError`, an index outside a list an `IndexError`, a unit mismatch at run time a `UnitError` (which is also a `ValueError`). Parent kinds match their children (`except LookupError` catches `KeyError` and `IndexError`). Your own error types are structs: `struct OutOfFuel: message: Str = ""` and `raise OutOfFuel("tank empty")` — `except OutOfFuel as e` then reads `e.message` and every other field of the struct. ### 7.3 Generators: `yield` A function that contains `yield` produces its values one at a time, only when they are asked for — so a sequence may be endless: ```apsi fn naturals(): var n = 0 while true: yield n n += 1 fn evens(xs): for x in xs: if x % 2 == 0: yield x yield from [100, 200] # hand on every value of another sequence for n in naturals(): if n > 4: break print(n) print(list(evens([1, 2, 3, 4]))) # [2, 4, 100, 200] ``` ### 7.4 `for … else` / `while … else` The `else` part runs when the loop finished without `break`: ```apsi for d in divisors: if n % d == 0: break else: print(f"{n} is prime") ``` Loop targets may be nested and starred: `for i, (a, b) in enumerate(pairs):`, `for first, *rest in rows:`. ### 7.5 `with` `with` enters a context and always leaves it, also when the block fails. Anything with `__enter__`/`__exit__` works (open files, locks, `torch.no_grad()`, your own classes); a struct with `enter`/`exit` methods works too. ```apsi with open("log.txt", "w") as f, lock: f.write("ok") ``` ### 7.6 `del`, assignment forms, `global` / `nonlocal` ```apsi del cache[key], obj.tmp, name a = b = 0 # several targets first, *rest = items # also: let first, *rest = items x |= flags # |= &= <<= >>= update in place where the value supports it ``` `global name` / `nonlocal name` say that a function assigns a variable of the module / of the enclosing function. In Apsi files they are optional (an outer `var` is assigned without them); converted Python keeps them. ### 7.7 Generators are lazy `(f(x) for x in xs)` is a generator: values are made one at a time, so `any(slow(x) for x in xs)` stops at the first true value and endless sources work. `[…]` is a list, `{…}` a set (`{x % 3 for x in xs}`) or a dict. ### 7.1 `parallel for` ```apsi @native fn total_energy(m, v) -> Float: var e = 0.0 parallel for i in 0..len(m): # iterations run on all CPU cores e += 0.5 * m[i] * v[i] * v[i] return e ``` `parallel for` promises that the iterations are independent, so they may run at the same time and in any order. In a `@native` function the iterations are spread over all CPU cores; in a `@gpu` function each iteration is one GPU thread. Everywhere else (the bytecode engine, the REPL) it runs like an ordinary `for`, so the same program works on every engine. - Variables declared inside the loop are private to each iteration. - An outer number may only be updated as a **reduction**: `x += …`, `x -= …`, `x *= …`, `x = min(x, …)` or `x = max(x, …)`. Each thread works on its own copy, and the copies are combined in a fixed order, so the result is identical on every machine and for every thread count. - Writing to tensor elements is allowed; two iterations must not write the same element. - `return`, `break` and `print` are not allowed inside (the native compiler runs such a loop sequentially and says why). - The loop must count over a range: `parallel for i in a..b`, `a..=b` or `range(…)`. `parallel` is a soft keyword: a variable called `parallel` keeps working. --- ## 8. Functions ### 8.1 Definitions ```apsi fn area(r: Length) -> Area = pi * r ** 2 # expression body fn solve(a, b = 1.0, tol = 1e-9): # block body var x = a while abs(x - b) > tol: x = (x + b) / 2 return x fn energy(m: Mass, v: Velocity) -> Energy = # expression body on the next line 0.5 * m * v ** 2 let double = fn(x) => x * 2 # anonymous function (lambda) ``` - A block body without `return` returns `none`. `return a, b` returns a tuple. - Parameters may have **defaults** (`b = 1.0`). Arguments may be passed by position or by name (`solve(1, tol=1e-6)`). Unknown names and wrong counts are reported by the checker. - Functions are values: pass them as arguments, return them, store them in lists and records. - Functions are **closures**: they capture the variables around them, and nested functions may modify outer `var`s. - Recursion is allowed (the limit is deep — 20,000 frames). ### 8.2 Type annotations Annotations on parameters, results and variables are **checked by the checker and again at run time**. | Annotation | Means | |---|---| | `Float`, `Int`, `Bool`, `Str`, `List`, `Tuple`, `Dict`, `Record`, `Fn`, `Any` | basic types (`Float` accepts `Int`) | | `Length`, `Mass`, `Time`, `Velocity`, `Force`, `Energy`, … | physical quantity of that dimension (list in 10.6) | | `[m/s]`, `[J/K]` | a dimension written as a unit | | `Tensor[B, T, D]` | tensor whose shape matches the symbols | | `Tensor[N, 3, Length]` | tensor with a unit for its elements | | `Tensor[_, 10, f32]` | `_` = any size; a dtype converts the tensor (see 13) | | `Vec3[Velocity]` | same as `Tensor[3, Velocity]` (`Vec`, `Vec2`, `Vec4`, `Mat`, `Matrix` exist too) | | a struct name | an instance of that struct | ### 8.3 Shape generics ```apsi fn attention[B, T, D](q: Tensor[B, T, D], k: Tensor[B, T, D], v: Tensor[B, T, D]) -> Tensor[B, T, D]: let scores = q @ transpose(k, (0, 2, 1)) / sqrt(D) return softmax(scores) @ v ``` Shape symbols (`B`, `T`, `D`) are bound to the real sizes at the call. Every argument must agree; otherwise a `ShapeError` names the dimension that differs. Inside the function the symbols are ordinary numbers (`sqrt(D)`). Writing `[B, T, D]` after the name is optional — symbols used in `Tensor[...]` are declared automatically. ### 8.3b Any number of arguments: `*args` and `**kwargs` ```apsi fn total(*xs, **opts): # xs is a List, opts a Dict var t = 0 for x in xs: t += x return t * opts.get("scale", 1) print(total(1, 2, 3)) # 6 print(total(1, 2, scale=10)) # 30 let nums = [4, 5] let o = {"scale": 2} print(total(*nums, **o)) # spread a list and a dict into a call: 18 ``` `*name` collects the remaining positional arguments, `**name` the remaining keyword arguments; both come after the ordinary parameters. Functions with them run on the bytecode engine (`@native` refuses them). ### 8.4 Decorators A decorator is any function that takes a function and returns a function; `@name` above `fn` applies it. | Decorator | Effect | |---|---| | `@precision(exact)` | every number in the function is an exact fraction — no rounding at all | | `@precision(f32)` / `f16` / `f64` | tensors entering and leaving the function are converted to that dtype | | `@precision(bf16)` / `fp8_e4m3` / `fp8_e5m2` / `fp6_e3m2` / `fp4_e2m1` / `int8` / `int4` | tensors are rounded to what the format can represent | | `@memo` | remember results by argument values | | `@timed` | print how long each call takes | | `@app.get("/path")` etc. | register a web route (chapter 22, `web`) | | `@disk_cache("dir")` | remember results on disk across runs (`use data`) | | `@native` | compile the function to machine code (see [`NATIVE.md`](/docs/2.4.4/native)); `@native("fast")` drops the safety checks and allows fast-math, `@native("strict")` makes "cannot compile" an error | | `@gpu` | run the function's `parallel for` on the NVIDIA GPU(s), or on all CPU cores when there is none ([`NATIVE.md`](/docs/2.4.4/native#gpu)) | ```apsi @memo fn fib(n: Int) -> Int = n if n < 2 else fib(n - 1) + fib(n - 2) print(fib(90)) # instant ``` --- ## 9. Structs and records ### 9.1 Structs ```apsi struct Particle: pos: Vec3[Length] vel: Vec3[Velocity] mass: Mass = 1kg # default value fn kinetic(self) -> Energy = 0.5 * self.mass * dot(self.vel, self.vel) fn moved(self, dt: Time): return Particle(self.pos + self.vel * dt, self.vel, mass=self.mass) let p = Particle(tensor([0.0, 0, 0]) * u.m, tensor([1.0, 0, 0]) * u.m / u.s) p.kinetic() # 0.5 J p.mass = 2kg # fields can be changed print(p) # Particle(pos=…, vel=…, mass=2.0 kg) ``` - Fields are declared as `name: Type` with an optional `= default`; fields with defaults come last. - Construct with positional or named arguments: `Particle(pos, vel, mass=3kg)`. Field types are checked. - Methods are `fn` definitions inside the struct; the first parameter is `self`. - Struct names can be used as types in annotations. ### 9.1b Operators on your own structs Methods with these names make a struct work with operators and built-ins — the same idea as Python's special methods (`apsi convert` renames `__add__` → `add`, `__str__` → `to_str`, and so on): | Method | Used by | | Method | Used by | |---|---|---|---|---| | `add sub mul div` | `+ - * /` | | `eq lt le gt ge` | `== < <= > >=` (and `sorted`) | | `floordiv mod pow matmul` | `// % ** @` | | `neg` | unary `-` | | `radd rmul rsub rdiv` | `3 + v` … | | `len get set contains` | `len(v)` `v[i]` `v[i] = x` `x in v` | | `iter` | `for x in v` | | `to_str` | `print`, `str()`, f-strings | | `call` | `v(...)` | | `hash bool` | dictionary keys, `if v:` | ```apsi struct Vec: x: Float y: Float fn add(self, o) = Vec(self.x + o.x, self.y + o.y) fn mul(self, k) = Vec(self.x * k, self.y * k) fn to_str(self) = f"Vec({self.x}, {self.y})" print(Vec(1.0, 2.0) + Vec(3.0, 4.0) * 2.0) # Vec(7.0, 10.0) ``` ### 9.2 Records `record(a=1, b=2)` creates an untyped container with named fields — ideal for model parameters: ```apsi let layer = record(W=randn(4, 8), b=zeros(8)) layer.W # field access ``` ### 9.3 Parameter trees (pytrees) Structs, records, lists, tuples and dicts nest into trees. `grad`, `value_and_grad`, `tree_map`, optimizers and `param_count` walk into every leaf, so a whole model is just a tree: ```apsi let model = [record(W=randn(2, 16), b=zeros(16)), record(W=randn(16, 1), b=zeros(1))] let doubled = tree_map(fn(x) => 2 * x, model) print(param_count(model)) # 65 ``` --- ### 9.4 Classes — Python's object model `class` defines a real class with the same rules as Python: inheritance from your classes *and from library classes* (`torch.nn.Module`, `threading.Thread`, `Exception`, `enum.Enum` …), `super()`, properties, static and class methods, special methods (`__add__`, `__eq__`, `__len__`, `__getitem__`, `__call__`, `__enter__` …), decorators on the class, metaclasses and keyword arguments in the header. ```apsi class Account: rate: Float = 0.02 # a field: class attribute + annotation (dataclasses use it) let opened = [0] # a class attribute fn __init__(self, owner, balance=0.0): self.owner = owner self._balance = balance # _private attributes are reachable inside your own classes Account.opened[0] += 1 @property fn balance(self): return self._balance fn __repr__(self): return f"Account({self.owner}, {self._balance})" class Savings(Account): fn __init__(self, owner): super().__init__(owner, 10.0) let dataclasses = pyimport("dataclasses") # a pure standard module: no permission needed @dataclasses.dataclass(frozen=true) class Point: x: Float y: Float = 0.0 ``` Methods see the scope around the class, not the class body (as in Python). Instances print with `__repr__` / `__str__`. A `struct` (9.1) is still the light, typed record for numeric code; a `class` is for objects with behaviour and for anything that has to fit into a Python library. ### 9.5 `use pycompat` — Python's exact semantics for a file A file that starts with `use pycompat` keeps Apsi's syntax but behaves exactly like Python: Python's `print`/`str`/ `repr` text (`True`, `None`, `'quotes'`), Python's built-ins (`range`, `map`, `zip`, `open` …), one namespace per function (a loop variable lives after the loop), default values evaluated once, `*args` as a tuple, `+=` in place, direct attribute access, `^` as exclusive or. `apsi convert` writes it at the top of every converted file; see [From Python to Apsi](/docs/2.4.4/python). Apsi's permissions still apply inside such a file. ## 10. Physical units ### 10.1 Writing quantities - letters directly after a number: `5m`, `2kg`, `300K`, `10ms`, `3eV`, `5µs` - brackets for compound units: `9.81[m/s^2]`, `8.314[J/(mol K)]`, `1[N*m]`, `5[m²]`, `3[kg m/s]` - inside brackets: `*`, `·` or a space multiply, `/` divides, `^` or `**` raise to a power (negative and fractional powers allowed: `^-1`, `^(1/2)`), and superscripts `² ³ ⁻¹` work - `u.` is one of that unit: `tensor([1, 2]) * u.km / u.h`, `x * u.eV` - **SI prefixes** work on base and derived units: Y Z E P T G M k h da d c m u/µ n p f a — `km`, `µs`, `MeV`, `kWh`, `GPa`, `mmol` ### 10.2 Available units `m g s A K mol cd Hz N Pa J W Wh C V F ohm/Ω S Wb T H eV L bar rad sr deg min h day yr au ly pc Å atm mmHg psi cal kcal Da ft inch mi lb mph kn rpm` ### 10.3 Rules - `+`, `-`, comparisons and `%` need equal dimensions — **except the number 0**, which matches any unit (so `var total = 0` can accumulate quantities). - `*`, `/` and `@` multiply and divide dimensions; `**` needs a dimensionless exponent. - **A dimensionless result becomes a plain `Float`**: `6[km] / 2[km]` is `3.0`. - `exp log sin cos tanh sigmoid softmax …` require dimensionless arguments; `sqrt` halves the dimension, `cbrt` thirds it. - Values are stored in SI units; printing picks a readable unit (N, J, W, Pa, …). - Unit errors are found by the checker before running whenever they can be proved; the rest are found at run time. ### 10.4 Conversion and access ```apsi let d = 12.5[km] d as [m] # 12500.0 m (display unit; must be the same dimension) (90deg) as [rad] # 1.5707963… — converting to a dimensionless unit gives a number d.magnitude # 12.5 (number in the displayed unit) d.value # 12500.0 (number in SI units) d.unit # "km" unit(d) dim(d) magnitude(d) ``` ### 10.5 Unit-aware functions Most built-ins accept quantities and give results with the right unit: `abs`, `sqrt`, `min`, `max`, `sum`, `mean`, `std`, `norm`, `dot`, `cross`, `linspace`, `arange`, `linalg.eigh` (eigenvalues of a Hamiltonian in J), `integrate` (unit [f]·[x]), `fft` frequencies (Hz when the step has a time unit), `grad` (∂Energy/∂Length is a Force). ### 10.6 Quantity types for annotations Dimensionless, Angle, Length, Mass, Time, Current, Temperature, Amount, LuminousIntensity, Area, Volume, Velocity / Speed, Acceleration, Jerk, Force, Energy / Work / Torque, Power, Pressure / Stress, Momentum / Impulse, AngularMomentum / Action, Frequency / AngularVelocity, Density, Charge, Voltage, Resistance, Capacitance, Inductance, MagneticField, MagneticFlux, ElectricField, Entropy / HeatCapacity, Viscosity, Diffusivity / KinematicViscosity, WaveNumber, Concentration, MolarMass, ThermalConductivity, SpringConstant. ### 10.7 Physical constants (`use physics.constants`) `c G h hbar k_B N_A R_gas q_e m_e m_p m_n epsilon_0 mu_0 g0 sigma_SB a_0 alpha_fs M_sun M_earth R_earth AU` — CODATA values with units. --- ## 11. Measurement uncertainty ```apsi let g = 9.81[m/s^2] ± 0.02[m/s^2] let L = 1.20m ± 0.01m let T = 2 * pi * sqrt(L / g) print(T) # (2.1975 ± 0.0094) s print(L - L) # (0 ± 0) m — correlations are tracked exactly ``` - `value ± σ` (or `value +/- σ`) creates a measured value that is an independent error source; σ is the standard uncertainty and must have the value's unit. - `±` binds tighter than `*` and `/`: `2 * 3.0 ± 0.1` is `2 * (3.0 ± 0.1)`. - Every operation (`+ - * / **`, `sqrt exp log sin cos tan tanh …`) propagates errors to first order **tracking each source separately**, so correlations are right: `x - x` is `0 ± 0`, `x * x` has twice the relative error of `x`. - `nominal(x)`, `sigma(x)` (keeps the unit), `relative_uncertainty(x)`, `measure(v, s)` (same as `v ± s`). - Comparisons use the nominal value. - `sci.curve_fit` returns fitted parameters as correlated `±` values. --- ## 12. Tensors and shapes ```apsi let A = tensor([[1.0, 2], [3, 4]]) # from nested lists (a list of quantities gives a tensor with a unit) zeros(3, 3) ones(2, 4) full((2, 2), 7.0) eye(3) zeros_like(A) arange(0, 10, 2) linspace(0, 1, 5) linspace(0m, 1m, 11) randn(64, 128, seed=1) rand(3) randint(0, 10, 5) A @ A # matrix product A * A # element-wise A.T # transpose shape(A), ndim(A), len(A) A[0, 1] A[:, 0] A[A > 2] reshape(A, 4) flatten(A) stack([A, A]) concat([A, A], axis=1) sum(A, axis=0) mean(A) max(A, axis=1, keepdims=true) argmax(A) ``` - Element types: `f64` (default), `f32`, `f16`, `i32`, `i64`, `bool`; pass `dtype=f32` to creation functions. - Arithmetic broadcasts like NumPy. - A tensor can carry one unit for all its elements: `tensor([1.0, 2.0]) * u.m`. - Functions and methods of the underlying NumPy array that do not start with `_` are available (`A.sum()`, `A.copy()`). - Shape annotations (`Tensor[B, T, D]`) are checked on every call (chapter 8.3). - `use gpu` moves tensors to an NVIDIA GPU (`to_gpu(x)`); every operator and autodiff work there too. --- ## 13. Precision and number formats | Format | Bits | Used for | |---|---|---| | `f64` | 64 | default; physics | | `f32` | 32 | AI, large simulations | | `f16`, `bf16` | 16 | inference / training | | `fp8_e4m3` (`fp8`), `fp8_e5m2` (`bf8`) | 8 | weights & activations / gradients | | `fp6_e3m2`, `fp6_e2m3` | 6 | OCP MX formats | | `fp4_e2m1` (`fp4`) | 4 | highly compressed weights | | `int8`, `int4`, `uint8` | 8 / 4 / 8 | quantization | | `exact` | – | exact fractions | - `@precision(fmt)` on a function (chapter 8.4); `Tensor[N, bf16]` in annotations. - `quantize(x, fmt)` stores real-size codes, `dequantize(q)` decodes, `round_to(x, fmt)` rounds with a straight-through gradient (quantization-aware training), `qmatmul(a, b)` multiplies quantized operands accumulating in FP32. - `exact("1/3")` creates an exact fraction; `to_float(x)` converts back. - Details and measurements: [`PERFORMANCE.md`](/docs/2.4.4/performance). --- ## 14. `evolve` — differential equations ```apsi var x = 1m var v = 0[m/s] let k = 4[N/m] let m = 1kg evolve x, v over t in 0[s]..10[s] step 1[ms] using rk4 record every 10 into traj: x' = v v' = -k / m * x print(x, v) # the final state print(traj.t[-1], traj.x[-1]) # recorded trajectory (tensors with units) ``` Form: `evolve STATE, … over TIME in START..END step DT [using METHOD [tol TOL]] [record [every N] into NAME]:` - State variables must be declared with `var`; after `evolve` they hold the final state. - Every state needs a derivative line `x' = expression`. **Its unit must be the unit of `x` divided by the unit of time** — checked by the checker and at run time. - The body may contain `let`, `if`, `for` and function calls; state variables are read-only inside it. The time variable (`t`) can be used in the expressions. - Methods: `euler`, `midpoint`, `rk4` (default), `rk45` (Dormand–Prince with adaptive steps: `step` is the first step, `tol 1e-10` the accuracy, default `1e-8`), `symplectic` (semi-implicit Euler, updating from the last state to the first, so order position, velocity), `verlet` (velocity Verlet; exactly two states: position and velocity). - `record [every N] into name` gives `name.t`, `name.x`, `name.v`, … as tensors with units. - **Speed**: the first step runs fully unit-checked; then a unit-erased version runs on plain SI numbers. Both are compared before the switch, and on any difference the checked version is used. ```apsi var y = 1.0 evolve y over t in 0[s]..5[s] step 0.5[s] using rk45 tol 1e-10 record into tr: y' = -y / 1[s] ``` --- ## 15. Automatic differentiation ```apsi fn potential(x: Length) -> Energy = 0.5 * 10[N/m] * x ** 2 let force = grad(potential) # a new function: dU/dx print(force(0.1m)) # 1.0 N — the unit is derived fn loss(w, b) = mean((X @ w + b - y) ** 2) let l, gs = value_and_grad(loss, wrt=[0, 1])(w, b) # gs[0] = ∂l/∂w, gs[1] = ∂l/∂b ``` - `grad(f, wrt=0)` returns a function computing ∂f/∂(argument `wrt`); `wrt` may be a list. - `value_and_grad(f, wrt=0)` returns `(value, gradient)`. - Arguments may be numbers, tensors, quantities or parameter trees (struct/record/list/dict); the gradient has the same shape. - **Gradients have correct units**: if `f` returns `Energy` and `x` is a `Length`, the gradient is a `Force`. - `if`, `for`, function calls and indexing work inside `f` (define-by-run). `f` must return a scalar. - **Any order**: `grad(grad(f))`, `hessian(f)`, `jacobian(f)`, `laplace(f)` (Σ ∂²f/∂xᵢ²); `grad` may be used inside a function that is itself being differentiated — enough for physics-informed neural networks (`examples/18_pinn.apsi`). - `stop_gradient(x)` blocks gradients; `derivative(f, x)` is the derivative of a scalar function at `x`; `nn.numeric_grad(f, x)` checks gradients with finite differences. - `conv2d` and `max_pool2d` are differentiable to first order only. --- ## 16. Modules and imports ```apsi use nn # bring every name of a module into scope, and also nn.name use linalg as la # or keep them under a short name only: la.eigh(H) use physics.constants # dotted module names use geom_tools # an installed library (apsi install geom-tools) import "mylib.apsi" # the public names (not starting with _) of another file import "lib/geometry.apsi" as geo ``` - `use NAME` brings every public name of the module into scope and, unless that name is already taken, also makes the module itself available as `NAME`, so both `ppo(…)` and `rl.ppo(…)` work after `use rl`. - `use` finds the standard modules listed in chapter 22 and installed libraries (`.apsil`, see [Libraries](/docs/2.4.4/libraries)). Dashes in a library name become underscores: `geom-tools` → `use geom_tools`. - `import` runs another `.apsi` file once (cached) and brings in its public top-level names. The path is relative to the importing file. Its `test` blocks do not run on import. - Circular imports are reported as errors. Standard modules: `math`, `physics`, `physics.units`, `physics.constants`, `physics.fields`, `physics.formulas`, `nn`, `layers`, `torch`, `rl`, `linalg`, `random`, `sci`, `special`, `stats`, `signal`, `mechanics`, `pde`, `quantum`, `medical`, `lab`, `precision`, `data`, `parallel`, `scale`, `web`, `models`, `gpu`, `sys`, `crypto`, `io`, `ffi`, `python`, `net`, `collections`, `memory`, `db`, `rt`, `hub`, `tokens`, `dataframe`, `env`. --- ## 17. Tests ```apsi fn mean_of(xs) = sum(xs) / len(xs) test "mean of three numbers": assert mean_of([1, 2, 3]) == 2 test "works with units": assert mean_of([1m, 3m]) ~= 2m ``` - `apsi run` skips `test` blocks; `apsi test` runs the file's top-level code first, then each test in that file's scope. - Put tests next to the code, or in `tests/*.apsi` files that `import` the code. - `testing()` is `true` while tests run — use it to skip side effects such as starting a server. - `apsi test` prints ✓/✗ per test with its time and exits with status 1 when something fails. --- ## 18. The checker and errors Before running, Apsi checks the whole file and reports **every** problem it can prove, without running any code: - undeclared names, including typos - assignments to `let` constants, parameters or functions - unit mismatches in `+ - < ==`, in arguments, in return values, in `var`s whose dimension changes, and in `exp/log/sin` of quantities with units - wrong argument counts and unknown keyword names - `evolve` rules (`var` states, a derivative for every state, derivative units, step unit) - `return` outside a function, unknown types The checker never warns falsely: what it cannot prove is left to the run-time checks. Every error shows the file, line, column, the source line and a caret, followed by the call stack: ``` orbit.apsi:12:20: UnitError: argument 'v' of kinetic() must be Velocity [m/s], got Length [m] (2.0 m) let k = kinetic(3kg, 2m) ^ in simulate() ``` Error kinds: `SyntaxError`, `NameError`, `TypeError`, `UnitError`, `ShapeError`, `RuntimeError`, `AssertionError`, `PermissionError`. --- ## 19. Permissions and security Programs run with a permission profile. By default (`standard`) files can be read and written; Python, C, the network and environment variables are off. | Permission | Allows | Flag | |---|---|---| | `read` / `write` | files (optionally only some folders) | `--allow-read[=DIRS]`, `--allow-write[=DIRS]` | | `net` | `http_get`, `http_post_json`, `use web` servers | `--allow-net[=HOSTS]` | | `env` | `getenv` | `--allow-env[=NAMES]` | | `python` | `pyimport`, `to_torch`, `from_torch`, ONNX/HF/PyTorch models | `--allow-python` (`=all` for system modules) | | `ffi` | `cload` (C/C++/Rust libraries) | `--allow-ffi[=LIB@SHA256]` | Profiles: `sandbox` (nothing, not even files), `strict`, `standard` (default), `developer` (everything). Set the default with `apsi config --profile NAME`, per project in `apsi.toml`, or per run with `--profile`, `--sandbox`, `--allow-all`. `apsi audit FILE` lists what a program needs and the minimal flags. Code without a permission gets a `PermissionError` immediately — imported libraries cannot get around it. Attributes starting with `_` on outside objects are blocked. **Apsi Guard** scans every program before it runs (skip with `--trust`); `apsi scan PATH` scans on demand. Limits: `--max-memory 2GB`, `--timeout 30s`. Full details: [`SECURITY.md`](/docs/2.4.4/security). --- ## 20. Execution engines | Engine | How it works | Use it for | |---|---|---| | **native** (`@native`, `@gpu`, `apsi run --native`) | each function is specialised on its argument types and units, compiled through LLVM to machine code for the exact CPU (AVX2/AVX-512), cached on disk; `parallel for` uses all cores, `@gpu` the GPUs | numeric code: loops, tensors, physics, simulation — typically 100–3,500× faster than bytecode, on par with C/C++/Rust ([`NATIVE.md`](/docs/2.4.4/native)) | | `compile` (default for everything else) | the checked program is compiled to Python bytecode: scopes resolved at compile time, specialized type checks, and an **erased twin** of every function used in hot loops | everything; code the native compiler does not handle (strings, lists, records, I/O) | | `interp` | the tree-walking reference interpreter that defines the language's meaning | cross-checking the engines; the REPL | The engines work together in one program: native functions and bytecode functions call each other freely, and `evolve` calls native right-hand sides from its integration loop. A `@native` function that uses something the native compiler does not support says why (once) and runs on the bytecode engine, so adding `@native` never breaks a program. `apsi native explain app.apsi` lists which functions run as machine code. Select the reference interpreter with `--engine interp`. The engines pass the same test suite; native results are bit-identical to the bytecode engine for scalar arithmetic. Closures created in loops capture each iteration's value. Run-time errors always point back to the line and column in the `.apsi` file. Parsed programs are kept in a signed compile cache, and native machine code in a signed machine-code cache, so an unchanged program starts without recompiling; `apsi build` turns a program into an executable (`.exe`). Both are described under *Executables and the compile cache* in chapter 24. Neural networks built with `use layers` can also run on PyTorch (CPU or GPU) — see [`BACKENDS.md`](/docs/2.4.4/backends). --- ## 21. Built-in functions Available everywhere without `use`. Unless noted, functions accept numbers, tensors, quantities and values that are being differentiated alike. `apsi doc NAME` (or `:doc NAME` in the REPL) prints a name's signature. ### 21.1 Input, output and conversion | Function | Description | |---|---| | `print(*values, sep=" ", end="\n")` | write values to standard output | | `input(prompt="")` | read one line from standard input (without the newline); `none` at end of input | | `args()` | program arguments: `apsi run app.apsi a b` → `["a", "b"]` | | `exit(code=0)` | stop the program with an exit status | | `str(x)` · `int(x)` · `float(x)` · `bool(x)` | convert (`int("42")`, `float("1.5")`) | | `list(x=())` · `tuple(x)` · `dict(...)` · `set(x)` | containers from iterables / keyword arguments | | `any(xs)` · `all(xs)` · `iter(xs)` · `next(it[, default])` | truth tests and iterators | | `ord(c)` · `chr(n)` · `hex(n)` · `bin(n)` · `oct(n)` · `format(x, spec)` · `repr(x)` | characters, number bases, formatting | | `getattr(obj, name[, default])` · `hasattr(obj, name)` · `callable(x)` | reflection (names starting with `_` stay private) | | `type(v)` | type name as a string | | `error(msg)` | raise a `RuntimeError` with `msg` | | `testing()` | `true` while `apsi test` runs | | `platform()` | `"windows"`, `"linux"` or `"macos"` | | `clock()` | a monotonic time in seconds (subtract two calls to measure) | | `permissions()` · `security_policy()` | the permissions of the running program | ### 21.2 Sequences and collections | Function | Description | |---|---| | `len(x)` | number of elements (first axis for tensors) | | `range(a, b=none, step=1)` | integers as a list-like range (`0..n` is usually nicer) | | `enumerate(xs, start=0)` | `(index, item)` pairs | | `zip(*xs)` | tuples of corresponding elements | | `map(f, xs)` · `filter(f, xs)` | apply / keep where `f` is true (also pipeline steps in `use data`) | | `sorted(xs, key=none, reverse=false)` · `reversed(xs)` | ordering | | `unique(xs)` | distinct values in first-seen order | | `keys(d)` | keys of a dict, record or struct | | `chars(s)` · `join(xs, sep="")` | split into characters / join strings | | `record(**fields)` | a record | | `tree_map(f, *trees)` · `param_count(tree)` | map over / count the leaves of a parameter tree | | `pmap(f, xs, workers=none)` | parallel map on a thread pool | ### 21.3 Math | Function | Description | |---|---| | `abs sqrt cbrt exp log log10 log2` | elementary functions (`sqrt` halves units) | | `sin cos tan asin acos atan atan2(y, x) sinh cosh tanh` | trigonometry (radians; `deg` converts) | | `floor ceil round(x, digits=0)` | rounding | | `min max sum mean std var prod` (`axis=`, `keepdims=`) | reductions; `min`/`max` also take several arguments | | `cumsum(x, axis)` · `argmax(x, axis)` · `argmin(x, axis)` | cumulative sum, positions of extremes | | `fsum(xs)` | correctly rounded sum of a list or tensor (no accumulated rounding error; keeps units) | | `divmod(a, b)` · `pow(a, b[, m])` | quotient and remainder · power (modular with 3 arguments) | | `approx(a, b, rel=1e-6, atol=1e-12)` | approximate equality | | `exact(x)` · `to_float(x)` | exact fraction / back to float | | `pi tau e inf nan` | constants | ### 21.4 Units and uncertainty | Function | Description | |---|---| | `u.` | one of a unit: `u.km`, `u.eV`, `u.m / u.s` | | `unit(x)` · `dim(x)` · `magnitude(x)` | unit name, dimension name, number in the displayed unit | | `measure(value, sigma)` | same as `value ± sigma` | | `nominal(x)` · `sigma(x)` · `relative_uncertainty(x)` | parts of an uncertain value | ### 21.5 Tensors | Function | Description | |---|---| | `tensor(x, dtype=none)` | tensor from (nested) lists; a list of quantities gives a tensor with a unit | | `zeros(*shape)` · `ones(*shape)` · `full(shape, value)` · `eye(n)` · `zeros_like(x)` · `ones_like(x)` | constant tensors | | `arange(start, stop, step=1)` · `linspace(start, stop, n=50)` · `meshgrid(x, y)` | ranges (with units too) | | `randn(*shape, seed)` · `rand(*shape, seed)` · `randint(lo, hi, *shape, seed)` · `permutation(n, seed)` · `seed(n)` | random numbers (default seed 0, so runs are reproducible) | | `shape(x)` · `ndim(x)` · `reshape(x, *shape)` · `flatten(x)` · `transpose(x, axes)` | shape handling | | `swapaxes(x, a, b)` · `permute(x, *axes)` · `unsqueeze(x, axis)` · `squeeze(x, axis)` | axis handling with PyTorch's meaning (negative axes count from the end; all differentiable) | | `erf(x)` · `gelu(x, approximate="tanh"\|"none")` | error function (differentiable); GELU with the tanh approximation or exact (PyTorch's default) | | `stack(items, axis=0)` · `concat(items, axis=0)` · `roll(x, shift, axis)` · `where(cond, a, b)` · `clip(x, lo, hi)` · `sort(x)` | data manipulation | | `dot(a, b)` · `matmul(a, b)` · `outer(a, b)` · `cross(a, b)` · `norm(x, axis)` · `einsum(spec, *xs)` | linear algebra (einsum multiplies units and is differentiable) | | `f16 f32 f64 i32 i64` (+ `bf16 fp8 … int4`) | dtypes for `dtype=` and `@precision` | ### 21.6 AI and autodiff | Function | Description | |---|---| | `grad(f, wrt=0)` · `value_and_grad(f, wrt=0)` | reverse-mode gradients (chapter 15) | | `derivative(f, x)` · `jacobian(f)` · `hessian(f)` · `laplace(f)` | higher-order derivatives | | `stop_gradient(x)` | block gradient flow | | `relu sigmoid tanh softmax(x, axis) log_softmax(x, axis) gelu softplus` | activations | ### 21.7 Precision `quantize(x, fmt, scale="auto", axis)` · `dequantize(q)` · `round_to(x, fmt)` · `qmatmul(a, b)` · `nbytes(x)` · `quant_error(x, fmt)` · `format_info(fmt)` · decorators `precision(fmt)`, `memo`, `timed`. ### 21.8 Files and data (permission `fs`) `read_text(path)` · `write_text(path, text)` · `exists(path)` · `load_json(path)` · `save_json(path, value)` · `load_csv(path, skip_header=0)` · `save_csv(path, data, header="")` · `json_parse(text)` · `json_str(value)`. Relative paths are resolved against the running `.apsi` file. ### 21.9 Other languages and the network | Function | Permission | Description | |---|---|---| | `pyimport(name)` | python | import a Python module | | `to_py(x)` · `from_py(x)` | python | convert values (units become `{value, unit}`) | | `to_torch(x, device)` · `from_torch(t)` | python | exchange tensors with PyTorch | | `cload(path)` · `library_path(path)` | ffi | load a C library; `lib.fn(name, [arg types], result type)` | | `http_get(url, timeout=30)` · `http_post_json(url, data, timeout=30)` | net | HTTP requests | | `getenv(name, default=none)` | env | environment variables (secrets, API keys) | --- ## 22. Standard modules Use with `use NAME` (all names) or `use NAME as short` (`short.name`). ### `math` `abs sqrt cbrt exp log log10 log2 sin cos tan asin acos atan atan2 sinh cosh tanh floor ceil round pi tau e inf nan` and from `special`: `gamma lgamma erf erfc erfinv factorial comb beta besselj legendre` ### `collections` — data structures and functional helpers ```apsi use collections let words = Counter(text.split()) print(words.most_common(3)) # [("the", 120), ("a", 87), ("of", 60)] let by_city = DefaultDict(fn() => []) by_city[p.city].append(p) let todo = Heap(key=fn(t) => t.priority) # smallest priority first; max_heap=true for largest let recent = Deque(max=100) # keeps the newest 100 let cache = LRUCache(1000) ``` | Class | What it is | |---|---| | `Counter(xs)` | counts: `add(x, n)`, `c[x]` (0 when absent), `most_common(k)`, `total()`, `c + d`, `c - d`, `items()`, `to_dict()` | | `DefaultDict(make)` | a dict whose missing keys are created by `make()` | | `Deque(xs, max)` | double-ended queue: `push`, `push_left`, `pop`, `pop_left`, `peek`, `peek_left`, `rotate(n)`; with `max`, a ring buffer | | `Heap(xs, key, max_heap)` | priority queue: `push`, `pop`, `peek`, `push_pop`; equal priorities leave in arrival order | | `LRUCache(capacity)` | `get(k, default)`, `set(k, v)`, `has`, `delete`, `stats()` (hits, misses, hit rate); thread-safe | | `OrderedSet(xs)` | a set that keeps insertion order: `add`, `discard`, `has`, `\|`, `&`, `-` | | `BiMap(pairs)` | a one-to-one map: `get(k)`, `key_of(v)`, `set`, `delete` | These functions are built in (no `use` needed): `first(xs, pred, default)`, `last(xs, pred, default)`, `partition(xs, pred)` → (matching, rest), `take_while(xs, pred)`, `drop_while(xs, pred)`, `scan(xs, f, init)` (running results), `fold(xs, f, init)`, `flat_map(xs, f)`, `zip_longest(a, b, …, fill=none)`, `interleave(a, b, …)`, `distinct_by(xs, key)`, `min_by(xs, key)`, `max_by(xs, key)`, `sum_by(xs, key)`, `frequencies(xs)` (most common first), `compose(f, g, …)` (`compose(f, g)(x)` is `f(g(x))`), `partial(f, args…)`. ### Science, engineering and medicine `special` (special functions), `stats` (distributions, tests, regression, power), `signal` (filters, spectra, peaks), `mechanics` (integrators, Lagrangian/Hamiltonian dynamics, N-body, orbits, rigid bodies, beams, trusses), `pde` (heat, wave, advection, Poisson, Navier–Stokes, CG, BiCGSTAB), `quantum` (Schrödinger equation, operators, qubits), `medical` (pharmacokinetics, epidemiology, diagnostic accuracy, survival analysis, clinical formulas, ECG/HRV, DICOM) and `lab` (seeds, provenance, experiment logs, tables). Complete reference: [`SCIENCE.md`](/docs/2.4.4/science). `physics` also contains radiation, relativity, optics, electromagnetism, thermodynamics and decay formulas with units (listed there). ### `physics`, `physics.constants`, `physics.fields`, `physics.units` - constants (CODATA, with units): `c G h hbar k_B N_A R_gas q_e m_e m_p m_n epsilon_0 mu_0 g0 sigma_SB a_0 alpha_fs M_sun M_earth R_earth AU` - fields: `laplacian(u, dx, boundary="periodic"|"fixed")` (∇² in any number of dimensions) · `gradient(u, dx, axis=0)` · `meshgrid(x, y)` - `physics` contains both; `physics.units` exists for clarity (units are always available) ### `torch` — the PyTorch API, natively Layers (`Linear`, `Conv1d/2d`, pooling, `BatchNorm1d/2d`, `LayerNorm`, `RMSNorm`, `GroupNorm`, `Embedding`, `LSTM`, `GRU`, `RNN`, `MultiheadAttention`, `Transformer…`, `Sequential`, activations), the `F.` functions, losses with all their options, optimizers (`SGD`, `Adam`, `AdamW`, `Adamax`, `RMSprop`, `Adagrad`, `NAdam`, `RAdam`), every common learning-rate scheduler, `DataLoader` / `TensorDataset`, `state_dict` / `load_state_dict`, `value_and_grad_aux`, `clip_grad_norm`, `checkpoint` / `checkpoint_layer` and `Module(init, forward)`. Layers use PyTorch's parameter names and give the same results as PyTorch. Each layer has `init(seed)` → parameters and is called as `layer(params, x)`. Reference: [From Python and PyTorch](/docs/2.4.4/python). ### `rl` — reinforcement learning Environments (`cartpole`, `gridworld`, `bandit`), `gae`, `discounted_returns`, `ppo`, `dqn`, `reinforce`, `evaluate`, preference learning (`dpo_loss`, `ipo_loss`, `preference_loss`, `grpo_advantages`, `kl_penalized_rewards`, `ppo_clip_loss`, `sequence_log_prob`) and search (`mcts`, `beam_search`, `best_of_n`). Reference: [Reinforcement learning](/docs/2.4.4/rl). ### `nn` — neural networks | Function | Description | |---|---| | `dense(n_in, n_out, seed, scale)` | parameters `record(W, b)` (He initialization) | | `mlp_init(sizes, seed=0)` | a list of dense layers, e.g. `mlp_init([2, 64, 3])` | | `conv_init(c_in, c_out, k=3, seed)` · `conv2d(x, w, b, stride=1, padding=0)` · `max_pool2d(x, k=2)` | CNNs (x: N×C×H×W) | | `layer_norm(x, gamma, beta, eps)` · `norm_init(d)` · `batch_norm(x, gamma, beta, eps, axis)` · `rms_norm(x, gamma, eps)` | normalization | | `embedding_init(vocab, d, seed)` | embedding table (look up with `E[idx]`) | | `attention(q, k, v, mask)` · `causal_mask(T)` | scaled dot-product attention | | `dropout(x, p=0.1, train=true, seed)` | dropout | | `lstm_init(n_in, n_hidden)` · `lstm_cell(p, x, h, c)` · `lstm(p, xs)` | LSTM (xs: B×T×F → B×T×H) | | `gru_init(n_in, n_hidden)` · `gru_cell(p, x, h)` · `gru(p, xs)` | GRU | | `mse(pred, target)` · `cross_entropy(logits, labels)` · `accuracy(logits, labels)` · `one_hot(labels, k)` | losses and metrics | | `l1_loss` · `bce_with_logits(logits, y)` · `bce(prob, y)` · `nll_loss(log_probs, labels)` · `huber_loss(pred, target, delta)` | more losses (mean reduction, the same values as PyTorch) | | `loader(X, y, batch_size, shuffle, seed)` | mini-batches that are reshuffled every time the loader is iterated (one pass = one epoch) | | `sgd(lr, momentum)` · `adam(lr, b1, b2, eps)` · `adamw(…, weight_decay)` · `rmsprop(lr, decay, eps)` · `adagrad(lr, eps)` | optimizers: `params = opt.step(params, grads)` | | `set_lr(opt, lr)` · `cosine_lr(step, total, base, warmup, minimum)` · `step_lr(step, base, every, factor)` · `linear_warmup(step, base, warmup)` | learning-rate schedules | | `clip_grad_norm(grads, max_norm=1)` · `global_norm(grads)` | gradient clipping | | `train(loss_fn, params, data, opt, epochs, batch_size, seed, verbose, clip, schedule, eval_fn)` | ready-made training loop; returns `record(params, history)` | | `save_params(path, params)` · `load_params(path, like)` | save / load parameters (.npz) | | `numeric_grad(f, x, eps)` | finite-difference gradient check | | `spiral_data(n_per_class, classes, noise, seed)` · `make_moons` · `make_blobs` · `make_regression` | sample datasets | | `train_test_split(X, y, test=0.2, seed)` · `standardize(X, axis)` · `batches(X, y, size, seed)` | data preparation | | `relu gelu sigmoid tanh softmax log_softmax` | activations (also built-ins) | ### `layers` — any neural-network architecture Layers, composition (`sequential`, `residual`, `parallel`, `dag`), graph neural networks (`graph`, `gcn`, `sage`, `gat`, `gin`, `graph_pool`, `segment_sum/mean/max/softmax`), transformers, a model zoo (`mlp`, `resnet_mlp`, `cnn`, `lstm_classifier`, `transformer_lm`, `transformer_classifier`, `autoencoder`, `vae`, `gcn_net`, `gat_net`, `gin_net`, `siamese`) and training helpers (`fit`, `predict`, `summary`, `count_params`). `custom_layer(init, forward, name, parts)` turns two functions into a layer (models converted from PyTorch use it), and `load_torch_weights(model, params, state_dict)` copies PyTorch weights into an Apsi parameter tree. Every layer has `init(seed)` and is applied as `layer(params, x, train=false, graph=…)`. More layers: `mean_pool(axis)`, `bilinear`, `scale`, `select`. Backends: `fit(…, backend="numpy"|"torch"|"auto", device="auto"|"cpu"|"cuda"|"mps"|"xpu", precision="fp64"|"fp32"|"tf32"|"bf16"|"fp16", compile=false)`, `predict(…, backend=…)`, `devices()`, `torch_module(model, params)` → `torch.nn.Module`, `params_from_torch(model, module)`, `check_torch(device)`. The torch backend needs `--allow-python`. Complete reference with shapes, parameters and recipes: [`LAYERS.md`](/docs/2.4.4/layers); backends: [`BACKENDS.md`](/docs/2.4.4/backends). ### `linalg` `inv det solve eigh eig svd trace norm` — units are preserved (eigenvalues of a Hamiltonian are energies). ### `random` `seed randn rand randint permutation` ### `sci` — scientific computing | Function | Description | |---|---| | `einsum("ij,jk->ik", A, B)` | Einstein summation (differentiable, multiplies units) | | `fft ifft rfft irfft(x, n, axis)` · `fftfreq(n, d)` · `rfftfreq(n, d)` · `power_spectrum(x, d)` · `real imag conj` | Fourier transforms (frequencies in Hz when `d` is a time) | | `integrate(f, a, b, points=64)` | Gauss–Legendre quadrature (unit [f]·[x]) | | `trapz(y, x, dx)` · `cumtrapz(y, x)` | integrate sampled data | | `interp(x, xp, fp)` | linear interpolation | | `find_root(f, x0, tol, max_iter)` · `bisect(f, a, b, tol, max_iter)` | root finding (Newton with exact autodiff derivatives) | | `minimize(f, x0, method="lbfgs"\|"adam", steps, tol, lr)` | minimize over any parameter tree; returns `record(x, value, steps, converged)` | | `curve_fit(model, x, y, p0, sigma)` | Levenberg–Marquardt; returns `record(params, cov, chi2, dof)` with correlated `±` parameters | | `solve_ivp(f, [t0, t1], y0, method="bdf"\|"rk45", rtol, atol, t_eval)` | initial value problems `y' = f(t, y)`: `bdf` is TR-BDF2 (implicit, L-stable, adaptive) for **stiff** systems such as chemical kinetics and circuits; `rk45` is adaptive Dormand–Prince; units are kept; returns `record(t, y, steps, rejected, evaluations, success)` | ### `precision` `quantize dequantize round_to qmatmul nbytes quant_error format_info` and the formats `f64 f32 f16 bf16 fp8_e4m3 fp8_e5m2 fp6_e3m2 fp6_e2m3 fp4_e2m1 int8 int4 uint8` (short names `fp8 bf8 fp6 fp4 e4m3 e5m2 fp16 fp32 fp64`). ### `data` — pipelines and storage | Kind | Functions | |---|---| | sources | `from_tensors(*arrays, batch, shuffle, seed, drop_last)` · `from_list(xs)` · `from_range(n)` · `from_generator(f)` · `from_csv(path, skip_header, chunk, delimiter)` · `from_jsonl(path)` · `from_parquet(path, columns, rows)` (needs `apsi pkg install data`) · `from_npy(path)` · `from_files(pattern)` · `interleave([pipelines])` | | steps (methods or `\|>`) | `map(f, workers, ordered)` · `filter(f)` · `batch(size, drop_last)` · `unbatch()` · `shuffle(buffer, seed)` · `prefetch(n)` · `take(n)` · `skip(n)` · `repeat(times)` · `cache(directory, shard)` · `window(size, shift)` (sliding windows for time series) · `shard(count, index)` (split across workers or machines) | | results | `.to_list()` · `len(ds)` · `throughput(ds, limit)` · iterate with `for` | | storage | `open_tensor(path, shape, dtype, mode)` (memory-mapped) · `load_tensor(path, mmap)` · `save_tensor(path, x)` · `flush(x)` · `@disk_cache(directory)` | ### `parallel` — concurrency `spawn(f, *args)` → task (`.wait()`, `.done()`) · `wait_all(tasks, timeout)` · `wait_any(tasks, timeout)` · `channel(capacity)` (`send`, `recv`, `close`, iterate with `for`) · `lock()` (`acquire`, `release`, `run(f)`) · `atomic(value)` (`add`, `get`, `set`) · `parallel_for(items, f, workers)` · `parallel_reduce(items, f, combine, init)` · `set_threads(n)` · `cpu_count()` · `sleep(t)` · `bench(f, repeat, warmup)` ### `web` — HTTP servers `server(name, max_body, workers, max_connections, log, handler_timeout, header_timeout, body_timeout, keepalive_timeout, security_headers)` · `response(body, status, headers, type)` · `redirect(location, status)` · `abort(status, message)` · `jwt_sign(claims, secret, expires_in)` · `jwt_verify(token, secret, leeway)` · `bearer_token(req)` · `require_jwt(secret, exempt)` · `require_api_key(keys, header, exempt)`. Server methods: `get post put patch delete route(method, path)` (as calls or decorators), `before`, `after`, `rate_limit`, `cors`, `static`, `health`, `expose_metrics`, `stats`, `handle` (call without the network, for tests), `run(host, port, tls_cert, tls_key, processes, workers)`, `run_background(port)`. Requests: `req.method path params query headers header(name) body text json() client id user`. Full guide: [`SERVER.md`](/docs/2.4.4/server). ### `models` — model files `save_model(path, params, metadata, shard, sign)` · `load_model(path, verify, key)` · `model_info(path)` (Apsi checkpoints) · `save_gguf(path, params, metadata, dtype, name, architecture)` · `load_gguf(path, nested)` · `gguf_info(path)` · `load_safetensors(path, dtype, names, nested)` · `save_safetensors(path, params, metadata)` · `safetensors_info(path)` · `quantize_model(params, dtype, min_size)` · `dequantize_model(params)` · `model_size(params)` · `qlinear(x, W, b)` · `onnx_model(path, providers)` · `hf_pipeline(task, model, device)` · `torch_load(path)` (the last three need `--allow-python`). Guides: [`AI.md`](/docs/2.4.4/ai), [`MODEL-FILES.md`](/docs/2.4.4/model-files). ### `gpu` and `sys` `device()` · `to_device(x, dev)` · `to_gpu(x)` · `to_cpu(x)` · `gpu_available()` · `gpu_info()` · `hardware()` (CPU, SIMD, GPU, RAM) · `cpu_threads()` ### `crypto` | Function | Description | |---|---| | `hash(data, algo="sha256")` | sha256, sha512, sha3_256, sha3_512, blake2b, blake2s | | `hmac_sign(key, message)` · `hmac_verify(key, message, signature)` | message signatures | | `password_hash(password)` · `password_verify(password, stored)` | salted scrypt | | `secure_equal(a, b)` | constant-time comparison | | `random_token(n)` · `random_hex(n)` · `secure_randint(lo, hi)` | randomness from the operating system's CSPRNG | | `b64encode(text)` · `b64decode(text)` | base64 | ### `io`, `ffi`, `python`, `net` Group the built-ins of 21.8 and 21.9 so they can be used under a name: `use io as fs` → `fs.read_text(path)`. `ffi` also has the C types `f64 f32 i32 i64 void cstr ptr_f64 ptr_f32 ptr_i32 ptr_i64`. --- ## 23. The REPL Start it with `apsi` (or `apsi repl`). Type Apsi code; the value of an expression is printed, and `_` holds the last value. ```text C:\Users\me apsi> let g = 9.81[m/s^2] C:\Users\me apsi> 0.5 * g * (2s) ** 2 19.62 m C:\Users\me apsi> cd C:\Users\me\project C:\Users\me\project apsi> ls Folder: C:\Users\me\project Modified Size Name ---------------- --------- ---- 2026-11-08 13:40 src\ 2026-11-08 13:41 412 B apsi.toml 1 folder, 1 file (412 B) C:\Users\me\project apsi> run src\main.apsi ``` The prompt shows the current folder, as a shell does (`~` stands for your home folder on Linux and macOS; long paths are shortened in the middle). `APSI_PROMPT=short` shows only `apsi>`. **Multi-line input** continues automatically after a line ending in `:` (a block — finish it with an empty line), inside open brackets or a triple-quoted string, and after a trailing `\`, `=`, `,`, `|>` or operator. `:paste` accepts any block until a line containing `:end`. **Shell words** behave like a terminal when they are not names you defined: `cd PATH` (Windows paths, quotes, `~`, `%VAR%`/`$VAR` and `cd -` work; the prompt shows the new folder), `pwd`, `ls`/`dir [PATH]` (folders first, then files, with modification time, size and a summary; `ls -a` includes hidden files), `clear`/`cls`, `run FILE`, `help`, `exit`/`quit`. `!COMMAND` runs any operating-system command (`!git status`). **Commands** (`:help` lists them; most have short aliases): | Command | Does | |---|---| | `:help [NAME]` (`:h`, `:?`) | help, or documentation for NAME | | `:quit` (`:q`, `exit`, Ctrl+D) | leave | | `:vars` (`:v`) | variables and functions of the session with their types | | `:type EXPR` (`:t`) | type of an expression | | `:doc NAME` | signature and documentation | | `:time [CODE]` | time one input, or toggle timing of every input | | `:bench [N] CODE` | benchmark code (N runs, or about one second) | | `:load FILE` (`:l`) · `:reload` (`:r`) | run a file inside the session, keeping its definitions | | `:run FILE` | run a file in a fresh interpreter | | `:check FILE…` · `:test [PATH…]` · `:fmt FILE…` | toolchain commands | | `:edit [FILE]` | open `$EDITOR` (Notepad on Windows), then run what you wrote | | `:paste` | paste a large block, finish with `:end` | | `:save [FILE]` | save every successful input of the session as a `.apsi` file | | `:history [N]` (`:hist`) | the last N inputs | | `:ast CODE` · `:tokens CODE` | syntax tree / tokens | | `:reset` | clear all variables and functions | | `:perms` · `:env` | permissions / versions and current folder | | `:cd` `:pwd` `:ls` `:clear` | the shell words, as commands | **Editing**: Tab completes names, fields (`p.`), `:commands` and paths; Up/Down recall history (kept in `~/.apsi/repl_history`); Ctrl+C cancels the current input. Colours turn off with `NO_COLOR=1`. The REPL uses the reference interpreter and the same permissions flags as `apsi run` (`apsi repl --allow-net`). --- ## 24. The command line ```text apsi [COMMAND] [OPTIONS] ``` ### 24.1 Shortcuts | You type | Means | |---|---| | `apsi` | REPL (or, when a program is piped in, run it: `cat app.apsi \| apsi`) | | `apsi FILE.apsi [ARGS…]` | `apsi run FILE.apsi ARGS…` | | `apsi -e "CODE"` (`-c`, `--eval`) | `apsi eval "CODE"` | | `apsi -i` | `apsi repl` | | `apsi TASK [ARGS…]` | `apsi task TASK ARGS…` when TASK is in `apsi.toml` | | `apsi -V`, `apsi --version` | version | A mistyped command prints suggestions ("did you mean: test"). ### 24.2 Running | Command | Description | |---|---| | `apsi run [FILE] [ARGS…]` | check, then run. Without FILE: the project's `main`. `-` reads the program from stdin. Options: `--no-check`, `--time`, `-w/--watch`, `--native` (machine code for every function that allows it), `--threads N`, `--engine compile\|interp`, security flags | | `apsi eval CODE…` | evaluate code; the value of a final expression is printed. `;` or `\n` separate statements | | `apsi repl` | interactive REPL (chapter 23) | | `apsi watch [PATHS…] [--do run\|test\|check] [--ext .apsi,.csv] [--interval S] [--no-clear] [-- FLAGS]` | re-run whenever a watched file changes | | `apsi bench FILE [-n RUNS] [--warmup N]` | best / median / mean ± sd / worst over several runs (output suppressed) | | `apsi build FILE [-o DIR] [--name N] [--allow-…] [--standalone] [--include PKG] [--add PATH] [--windowed]` | make an executable — see *Executables and the compile cache* below | | `apsi compile [PATHS] [--runtime] [--stats] [--clear]` | parse, check and compile programs now, so later runs start from the compile cache | | `apsi serve FILE [--host H] [--port P] [--only NAMES…]` | expose the program's functions as a JSON HTTP API | #### Executables and the compile cache **How a program runs.** `apsi app.apsi` parses the program, checks it, compiles it to Python bytecode (engine `compile`, the default) and runs it. Parsed programs are kept in the **compile cache** — in memory, so the Guard scan, the checker, imports and the run parse each file only once, and on disk in `~/.apsi/cache/compiled`, so the next run of an unchanged file skips parsing. Disk entries are signed with your per-user key (`~/.apsi/cache.key`, HMAC-SHA256); an entry that was modified or copied from another computer is deleted, never loaded. The cache is keyed by the file's contents and by the Apsi version, so it can never serve a stale result. `apsi compile PATHS` fills it ahead of time (and checks every file), `apsi compile --runtime` also compiles Apsi itself to bytecode, `apsi compile --stats` / `--clear` show and empty it, and `APSI_NO_CACHE=1` turns it off. **Executables.** `apsi build app.apsi` turns a program into one executable file: ```bash apsi build app.apsi # app.exe next to app.apsi (Windows), ./app (Linux) apsi build train.apsi --allow-python -o dist # the permissions are fixed when it is built apsi build app.apsi --standalone # Windows: dist folder with its own Python, for PCs without Apsi apsi build server.apsi --windowed --allow-net # no console window apsi build app.apsi --add data # pack a data folder (it sits next to the program when it runs) ``` The executable contains the program and every `.apsi` file it imports, the already parsed and checked program, Apsi itself compiled to bytecode (so it runs exactly the Apsi version it was built with) and the permissions given to `apsi build`. A program with check errors is never built. Arguments given to the executable go to the program (`args()`), all of them — `app.exe --verbose x` passes `["--verbose", "x"]`. On the first start the program's files are unpacked once into `~/.apsi/apps/NAME-ID`; later starts use them directly. | Option | Meaning | |---|---| | `-o DIR`, `--name N` | output folder and name | | `--allow-read`, `--allow-net`, `--allow-python`, `--allow-all`, `--profile`, … | permissions of the built program | | `--standalone` | Windows: `DIR\NAME\NAME.exe` plus `python\` with the Python runtime, numpy and the Python packages the program uses (`pyimport("…")`, the torch backend — detected automatically; `--include PKG` adds more). Copy the whole folder to another PC. | | `--windowed` | Windows: no console window | | `--add PATH` | pack a data file or folder (inside the program's folder) | Without `--standalone` the executable uses the Python runtime of the Apsi installation that built it, so it runs on computers where Apsi is installed in the same place. ### 24.3 Checking, testing, formatting | Command | Description | |---|---| | `apsi check [PATHS…] [-q]` | check files and folders without running (default: current folder) | | `apsi test [PATHS…]` | run test blocks (default: the project's `tests/`); accepts security flags and `--engine` | | `apsi fmt [PATHS…] [--check]` | format in place; `--check` only reports (exit 1 when something would change) | | `apsi audit FILES…` | the capabilities a program uses and the minimal `--allow-*` flags | | `apsi ast FILE` · `apsi tokens FILE` | syntax tree / tokens | ### 24.4 Projects and documentation | Command | Description | |---|---| | `apsi new NAME [-t app\|lib\|physics\|ai\|server] [--no-vscode] [--force]` | create a project folder | | `apsi init [PATH] [--name N] [-t TEMPLATE]` | turn a folder into a project | | `apsi task [NAME] [ARGS…]` | list or run `apsi.toml` tasks | | `apsi doc [NAME] [FILTER]` | modules, built-ins, or one name: `apsi doc`, `apsi doc nn`, `apsi doc nn adam`, `apsi doc sqrt` | | `apsi help [COMMAND]` · `apsi COMMAND --help` | help | | `apsi completion bash\|zsh\|fish\|powershell` | shell completion script | | `apsi convert PATHS… [-o OUT] [--run] [--stdout] [--no-check]` | translate Python files or folders into Apsi, mapping numpy/math/torch/… onto Apsi (see [`PYTHON.md`](/docs/2.4.4/python)) | | `apsi editors [list\|install\|uninstall\|path] [NAMES…] [--json]` | Apsi support for VS Code, Cursor, Windsurf, VSCodium, VS Code Insiders, Trae, Sublime Text, Neovim, Vim and Notepad++ (see [`EDITORS.md`](/docs/2.4.4/editors)) | Install completion: bash `eval "$(apsi completion bash)"` in `~/.bashrc`; zsh `eval "$(apsi completion zsh)"`; fish `apsi completion fish > ~/.config/fish/completions/apsi.fish`; PowerShell `apsi completion powershell | Out-String | Invoke-Expression` in `$PROFILE`. ### 24.5 Security, packages and the machine | Command | Description | |---|---| | `apsi scan PATHS… [--av] [--json] [--fail-on LEVEL]` | malware scan of programs and folders | | `apsi verify` | compare the installed runtime with its release manifest | | `apsi config [--profile sandbox\|strict\|standard\|developer]` | show or set the default profile | | `apsi pkg list \| install NAMES \| remove NAMES \| lock \| audit` | optional Python libraries (`gpu onnx torch transformers sci data plot crypto audit`, groups `ai ai-gpu science all`) | | `apsi doctor [--json]` | check this machine against Apsi's hardware targets | | `apsi plan PARAMS [--active N] [--gpu G] [--gpus N\|--days D]` | plan a large-model training run: layout, memory per GPU, GPU count, days ([`SCALE.md`](/docs/2.4.4/scale)) | | `apsi model info\|verify\|convert FILE [OUT] [--dtype T] [--shard S] [--key K]` | inspect, verify and convert model files: GGUF, `.apsim`, safetensors ([`MODEL-FILES.md`](/docs/2.4.4/model-files)) | | `apsi native info\|explain\|ir\|asm\|clear-cache` | the native compiler: this machine's CPU/SIMD/threads/GPUs; which functions of a program run as machine code and why not; the LLVM IR or assembly of a function; empty the machine-code cache | | `apsi lsp` | language server over stdio (for editors) | | `apsi version` | version | ### 24.6 Security flags (run, test, repl, eval, bench, serve) `--profile NAME` · `--allow-read[=DIRS]` · `--allow-write[=DIRS]` · `--allow-net[=HOSTS]` · `--allow-env[=NAMES]` · `--allow-python[=all]` · `--allow-ffi[=LIB@SHA256]` · `--allow-all` · `--sandbox` · `--trust` (skip Apsi Guard) · `--max-memory SIZE` · `--timeout TIME` · `--engine compile|interp` ### 24.7 Exit status `0` success · `1` errors (check failed, test failed, program error) · `2` usage error (bad arguments, missing file) · `3` blocked by Apsi Guard · `130` interrupted (Ctrl+C) · any value passed to `exit(code)`. ### 24.8 Environment variables | Variable | Effect | |---|---| | `APSI_HOME` | settings folder (default `~/.apsi`: config, REPL history, cache key) | | `NO_COLOR` / `APSI_NO_COLOR` | no colours; `FORCE_COLOR` forces them | | `EDITOR` / `VISUAL` | editor for `:edit` | --- ## 25. Projects and `apsi.toml` `apsi new orbit --template physics` creates: ```text orbit/ apsi.toml project settings and tasks src/main.apsi entry point tests/main_test.apsi tests (import "../src/main.apsi") README.md .gitignore .editorconfig .vscode/ tasks.json (run, test, check, format, watch), settings.json, extensions.json ``` Templates: `app` (default), `lib`, `physics`, `ai`, `server`. ```toml [project] name = "orbit" version = "0.1.0" description = "" main = "src/main.apsi" # what `apsi run` runs when no file is given [run] profile = "standard" # sandbox | strict | standard | developer allow = ["net"] # extra permissions for run/test in this project: read write net env python ffi [tasks] # apsi task NAME — or simply apsi NAME start = "apsi run src/main.apsi" test = "apsi test tests" check = "apsi check src" fmt = "apsi fmt src tests" ci = ["apsi check src", "apsi fmt --check src tests", "apsi test tests"] ``` - `apsi run`, `apsi test` and `apsi task` find `apsi.toml` in the current folder or any parent folder. - A task is a command line or a list of command lines run in order (stopping at the first failure), in the project folder. - Command lines that start with `apsi` use the same Apsi that runs the task, so tasks work even when `apsi` is not on PATH. Other command lines run in the system shell. - Extra arguments go to the last step: `apsi task start --time`. - Flags given on the command line override `[run]`. --- ## 26. Style guide `apsi fmt` applies these rules automatically. - 4 spaces per indentation level; no tabs. - `snake_case` for variables and functions, `PascalCase` for structs, `UPPER_CASE` for module-level constants that you want to stand out; single-letter physics names (`m`, `v`, `F`, `T`) are fine. - spaces around `= == -> => |> ±` and binary operators; none inside unit literals (`9.81[m/s^2]`), ranges (`0..10`) or keyword arguments (`f(x=1)`). - annotate the units of public functions: `fn kinetic(m: Mass, v: Velocity) -> Energy`. - put tests next to the code they test. - prefer `let`; use `var` only for values that change. --- ## 27. Grammar The grammar in EBNF (`{ }` = repeat, `[ ]` = optional, `|` = choice). `INDENT`/`DEDENT`/`NEWLINE` come from the indentation rules of chapter 3. ```ebnf program = { statement } ; statement = let | fn_def | struct_def | if | while | for | evolve | test | "return" [ expr_list ] | "break" | "continue" | "pass" | "use" dotted [ "as" NAME ] | "import" STRING [ "as" NAME ] | "assert" expr [ "," expr ] | NAME "'" "=" expr | expr_list [ ( "=" | augop ) expr_list ] ; let = ( "let" | "var" ) NAME { "," NAME } [ ":" type ] "=" expr_list ; fn_def = { "@" expr NEWLINE } "fn" NAME [ "[" names "]" ] "(" params ")" [ "->" type ] ( "=" expr | block ) ; params = [ param { "," param } ] ; param = NAME [ ":" type ] [ "=" expr ] ; struct_def = "struct" NAME ":" NEWLINE INDENT { NAME ":" type [ "=" expr ] NEWLINE | fn_def } DEDENT ; if = "if" expr block { "elif" expr block } [ "else" block ] ; while = "while" expr block ; for = "for" names "in" expr block ; evolve = "evolve" names "over" NAME "in" expr "step" expr [ "using" NAME [ "tol" expr ] ] [ "record" [ "every" expr ] "into" NAME ] block ; test = "test" STRING block ; block = ":" NEWLINE INDENT { statement } DEDENT | ":" statement ; type = NAME [ "[" type_arg { "," type_arg } "]" ] | "[" unit "]" ; augop = "+=" | "-=" | "*=" | "/=" | "//=" | "%=" | "**=" | "@=" ; expr = "fn" "(" params ")" "=>" expr | pipe ; pipe = ternary { "|>" ternary } ; ternary = coalesce [ "if" coalesce "else" ternary ] ; coalesce = or { "??" or } ; or = and { "or" and } ; and = not { "and" not } ; not = "not" not | comparison ; comparison = range { cmp_op range } ; cmp_op = "==" | "!=" | "<" | "<=" | ">" | ">=" | "~=" | "in" | "not" "in" ; range = additive [ ( ".." | "..=" ) additive ] { "as" "[" unit "]" } ; additive = term { ( "+" | "-" ) term } ; term = unary { ( "*" | "/" | "//" | "%" | "@" ) unary } ; unary = ( "-" | "+" ) unary | plusminus ; plusminus = power [ ( "±" | "+/-" ) power ] ; power = postfix [ ( "**" | "^" ) unary ] ; postfix = primary { "(" args ")" | "[" subscripts "]" | "." NAME | "?." NAME | "?[" subscripts "]" } ; primary = NUMBER [ unit ] | STRING | FSTRING | NAME | "true" | "false" | "none" | "(" [ expr_list ] ")" | "[" list_or_comp "]" | "{" dict_or_comp "}" ; args = [ arg { "," arg } ] ; arg = expr | NAME "=" expr ; ``` # Native code: `@native`, `parallel for` and `@gpu` Apsi compiles functions to machine code. A function marked `@native` is specialised on the types and units of its arguments, translated to LLVM IR, optimised at `-O3` for the exact CPU it runs on (AVX2, AVX-512, FMA, NEON), and called directly — no interpreter in between. `parallel for` spreads a loop over all CPU cores, and `@gpu` runs it on NVIDIA GPUs. ```apsi use physics.constants @native fn orbit_speed(m: Mass, r: Length) -> Velocity = sqrt(G * m / r) @native fn energy(x, v, k: Float) -> Float: var e = 0.0 parallel for i in 0..len(x): # all CPU cores e += 0.5 * v[i] * v[i] + 0.5 * k * x[i] * x[i] return e print(orbit_speed(5.972e24kg, 384400[km]) as [km/s]) # 1.01828904573 km/s ``` Nothing else changes: native functions take and return ordinary Apsi values, call and are called by any other function, and work inside `evolve`. Results are identical to the bytecode engine. - [How it works](#how-it-works) - [Performance](#performance) - [What compiles](#what-compiles) - [Semantics and safety](#semantics-and-safety) - [Tensors](#tensors) - [Units](#units) - [`parallel for`](#parallel-for) - [`@gpu`](#gpu) - [Tools and settings](#tools-and-settings) - [Limits](#limits) --- ## How it works ```text .apsi source ──▶ parser ──▶ checker (names, units, types) ──▶ checked syntax tree │ first call with new argument types (Int, Float, f32, tensors of a dtype/rank/layout, units) ▼ specialiser: infers every variable's type by abstract execution, checks units statically and erases them, proves index ranges, fuses tensor expressions, outlines parallel loops │ ▼ LLVM IR ──▶ LLVM -O3 (inlining, loop + SLP vectorisation, unrolling) for the host CPU ──▶ machine code │ (signed on-disk cache) ▼ NativeFunction: checks argument types/units at the boundary, calls the machine code, restores units on the result ``` - **Specialisation.** `f(2.0)` and `f(tensor)` get separate machine code, each as fast as hand-written code for that type. The first call compiles (typically 10–50 ms); later calls go straight to machine code. - **Machine-code cache.** Compiled code is stored in `~/.apsi/cache/native`, signed with HMAC-SHA256 using your per-user key; an entry whose signature does not verify is never loaded. The cache key covers the IR, the CPU model and features, and the LLVM and Apsi versions, so code is only reused where it is valid. - **Fast calls.** A cheap check of the argument types finds the machine code directly; numbers are passed in registers and tensors as the NumPy object itself (the machine code reads its data pointer, shape and strides from NumPy's struct, whose layout is measured and verified at start-up). A call costs ~0.9 µs, ~1.1 µs with a tensor (it was ~8–10 µs in the first 2.0 build). - **Never breaks a program.** If a function uses something the native compiler does not handle, Apsi prints one note with the reason and line, and that function runs on the bytecode engine. Arguments native code cannot take (lists, records, structs, values being differentiated by `grad`) also use the bytecode engine, so `grad(f)` of a native `f` works. - **Translated once, then direct.** A processor only executes its own machine instructions, so every language is translated at some point: C, C++, Rust and Go before you ship, Apsi the first time a function is called with new argument types. LLVM IR is the in-memory form the optimiser works on, not an extra layer at run time. After that first call — and in every later run, through the signed cache — the call goes straight into machine code with no translation, interpretation or Python in between. Measured on a 2-vCPU machine: the first compilation of a small loop takes about 90 ms; later runs load it from the cache in about 30 ms, most of which is loading the LLVM library once per process; each call then costs under 1 µs. - **One program, two engines.** Native and bytecode functions call each other freely. Calls between native functions are direct machine-code calls, usually inlined. ## Performance Same algorithms, same inputs, best of 5 runs, milliseconds (lower is better). Every language produces the same checksums. Measured on a 2-vCPU cloud machine (Intel Xeon, Emerald Rapids, AVX-512); GCC 13.3, Clang 18.1, Rust 1.97, Go 1.24, Apsi 2.0 with LLVM 22. | Benchmark | Apsi 1.x bytecode engine | Apsi `@native` | Apsi `@native("fast")` | C (gcc -O3) | C++ (g++ -O3) | C (clang -O3) | Rust (-O3) | Go | Apsi fast + `parallel for` | |---|---|---|---|---|---|---|---|---|---| | fib(32), recursion | 1421 | 14.1 | 6.7 | 3.5 | 3.7 | 5.2 | 5.0 | 10.0 | – | | Mandelbrot 1000×1000, 50 it. | 6302 | 60.1 | **46.9** | 61.8 | 65.8 | 57.3 | 59.5 | 62.5 | **24.5** | | spectral-norm 1000 | 16425 | 50.1 | **24.9** | 24.9 | 25.1 | 49.7 | 46.8 | 49.6 | **12.6** | | sieve 10M | 13545 | 61.9 | **50.9** | 53.6 | 55.0 | 51.0 | 57.6 | 59.0 | – | | n-body, 1M steps | 63520 | 67.0 | 45.9 | 57.6 | 55.7 | **38.7** | 65.5 | 79.1 | – | | matmul 512, naive i-k-j | 102456 | 35.6 | 28.9 | **27.4** | 41.0 | 36.7 | 46.7 | 144.5 | **15.0** | | `sum(sqrt(a*a + b*b))`, 4M | 46 | 11.1 | 4.0 | 4.7 | 6.6 | 10.2 | 11.1 | 10.4 | **2.2** (automatic) | The Apsi columns before the last one are single-core. The last column uses both cores of this machine: through `parallel for`, or automatically for large fused tensor expressions and reductions (the `sum(sqrt(…))` row runs on both cores without any change to the code; `@native` without `fast` takes 4.95 ms there). C, C++, Rust and Go were measured as written, single-threaded. How to read it: - **Against the 1.x bytecode engine:** 100× to 3,500× faster on loop-heavy code. - **Against C, C++, Rust and Go:** `@native("fast")` is the fastest or tied single-threaded result on four of the seven benchmarks and within 6–20 % of the best on n-body and matmul. Recursive `fib` remains about 2× behind GCC, which applies recursion-specific transformations, and is faster than Go. - **Default `@native`** keeps checks those languages' release builds do not have (64-bit integer overflow, every index, integer division by zero) and still lands in the same range as Clang, Rust and Go on most benchmarks. - **Why no compiler is 100× faster than C on the same algorithm:** C, C++ and Rust already compile to machine code that runs close to what the processor can do; Apsi uses the same LLVM optimiser. Beyond that point, more speed comes from more hardware: `parallel for` scales with the number of cores (this machine has 2; a 16-core desktop gives up to about 16× over single-threaded C), and `@gpu` uses the thousands of cores of a GPU. Apsi does this for you: large tensor kernels split across cores automatically, while in C, Rust or Go you would add threads by hand. Reproduce: `sh benchmarks/run_all.sh` (needs gcc, g++, rustc, go; clang optional). ### Heavy workloads against C, C++, Rust, Go and Mojo A second suite (`benchmarks/heavy/`) uses larger, harder programs: dense all-pairs gravity, a PDE stencil, a 1024² matrix product, LU factorisation with pivoting, Ising Monte Carlo, a 100-million-cell dynamic program, a 20-million-step Runge–Kutta integration and k-means clustering. Each is written the same way in Apsi (`@native`, strict IEEE), C, C++, Rust, Go and Mojo 1.2 (with raw pointers, as performance-oriented Mojo is written); the runner refuses to report times unless all of them produce the same checksum. Apsi 2.4.4 on a GitHub Actions runner (AMD EPYC 7763), single core, milliseconds, best of 5 rounds in which Apsi 2.4.4 and 2.0.0 ran in turn on the same runner (geometric mean: Apsi takes 0.81× the time of C): | Benchmark | Apsi | C | C++ | Rust | Go | |---|---:|---:|---:|---:|---:| | nbody-dense 2000x10 | 46.5 | **44.1** | **44.1** | 181.9 | 317.4 | | jacobi-2d 1024x200 | **151.4** | 167.1 | 157.3 | 166.6 | 700.7 | | matmul 1024 | 177.6 | 199.6 | **164.8** | 182 | 479.4 | | lu-pivot 800 | **40** | 40.3 | 40.1 | 41.2 | 116.5 | | ising 512x50 | **81.5** | 90.6 | 90.4 | 83.6 | 145.9 | | levenshtein 10k | **195.1** | 809.3 | 305.6 | 268.7 | 1,736 | | ode-rk4 20M | 354.9 | **288.7** | 288.8 | 354.9 | 355.2 | | kmeans 200k k16 x20 | **51.3** | 60.5 | 59 | 64.2 | 213.3 | The same runner, Apsi 2.0.0 against 2.4.4 (geometric mean: 2.4.4 is 1.29× faster; `@native` code was already at C speed in 2.0.0): | Benchmark | Apsi 2.0.0 | Apsi 2.4.4 | 2.4.4 against 2.0.0 | |---|---:|---:|---:| | nbody-dense 2000x10 | 182.7 | 46.5 | **3.93× faster** | | jacobi-2d 1024x200 | 150.5 | 151.4 | same (0.99×) | | matmul 1024 | 178.9 | 177.6 | same (1.01×) | | lu-pivot 800 | 40.3 | 40 | same (1.01×) | | ising 512x50 | 81.5 | 81.5 | same (1.00×) | | levenshtein 10k | 193.8 | 195.1 | same (0.99×) | | ode-rk4 20M | 533.3 | 354.9 | **1.50× faster** | | kmeans 200k k16 x20 | 65.3 | 51.3 | **1.27× faster** | jacobi-2d is limited by memory bandwidth, which other jobs on the shared runner also use, so one round can be much slower than the others (one of the five rounds took 261 ms); the best of several rounds is the reliable number. C's ode-rk4 is faster because gcc fuses multiply-adds by default; with `@native("fma")` Apsi takes 263 ms there. Results on another runner model (EPYC 9V45, AVX-512) and Mojo results from an earlier machine are in `benchmarks/heavy/results.md` and `benchmarks/heavy/versions/`. Run it on your own machine with `python3 benchmarks/heavy/run.py` (languages whose compiler is missing are skipped). ## What compiles | Area | Supported in native code | |---|---| | Values | `Int` (64-bit), `Float` (f64), `f32` scalars, `Bool`, tensors of `f64 f32 i64 i32 i16 i8 uint8 bool` of any rank (contiguous or strided views), tuples, physical quantities | | Statements | `let`, `var`, assignment (also to tensor elements and sub-tensors), `+= -= *= /= //= %= **=`, `if/elif/else`, `while`, `for` over ranges / `range()` / tensors / `enumerate(tensor)`, `parallel for`, `break`, `continue`, `return` (also of tuples and tensors), `assert`, `print` | | Expressions | arithmetic, comparisons, `and`/`or`/`not`, `a if c else b`, `~=`, `as [unit]`, indexing `a[i]`, `a[i, j]`, `a[i][j]`, negative indices, slices `a[lo:hi:step]`, `a.shape`, `a.size`, `a.ndim`, `a.T` | | Built-ins | `sqrt cbrt exp log log10 log2 sin cos tan asin acos atan atan2 sinh cosh tanh abs floor ceil round min max pow float int bool len shape ndim sum mean prod argmin argmax dot norm clip where relu sigmoid magnitude zeros ones full zeros_like ones_like` and the constants `pi e tau inf nan` | | Matrices | `a @ b` for vectors and matrices (f64 or f32) runs on the multi-threaded BLAS, the same kernels as NumPy | | Calls | other Apsi functions (compiled together, recursion included), physics constants and other global numbers and tensors, `module.name` constants such as `u.au` | Not compiled yet (the function runs on the bytecode engine and the note says which): strings other than in `print`/`assert`, lists, dicts, records, structs, lambdas and closures as values, comprehensions, `evolve` *inside* a native function (an `evolve` that *calls* native functions is fine), `@` on tensors with more than 2 axes, reductions with `axis=`, reading a global declared with `var` (pass it as an argument instead), and calls to Python libraries. ## Semantics and safety Native code means the same thing as the bytecode engine. Default `@native` keeps every check: | Situation | Native behaviour | |---|---| | `Int` arithmetic | 64-bit; overflow is an error with the source line (bytecode Ints never overflow); an argument too big for 64 bits runs the call on the bytecode engine | | `//` and `%` | floor division and modulo with the sign of the divisor, exactly as Apsi defines them, for Int and Float | | Division by zero | exactly as in the bytecode engine: for ordinary numbers `/`, `//`, `%` by zero and `0.0 ** -1` are `ZeroDivisionError`; for tensor elements (NumPy scalars) they give `inf`/`nan`, and `Int // 0` gives 0 | | Number origin | a value read from a tensor is a NumPy scalar in the bytecode engine, with NumPy's rules (above; Int overflow wraps around). Native code tracks where every value came from, at compile time or with a one-bit run-time flag when it depends on the path, and applies the same rule. The result even comes back as the same Python class (`float` or `numpy.float64`) | | Indexing | every index is checked; negative indices count from the end; loops whose index range can be proven safe up front run without per-element checks (the loop is versioned, so behaviour is unchanged) | | `min`/`max` | the same choice as the bytecode engine, including the order of equal values | | `round(x)` | round half to even, returns an Int | | Float arithmetic | strict IEEE 754, no reassociation and no FMA contraction, so results match the bytecode engine bit for bit | | Verified by | `tests/test_native.py` (every construct against the bytecode engine) and `tests/test_native_fuzz.py`: random Int/Float programs with Python and NumPy arguments must give the same values, the same errors and the same result classes — 0 differences in 14,400 calls with the default seeds and 8 more | | `Int ** Int` | exact; an exponent that turns out negative at run time is an error (write it as a Float), because the result would have to change type | | Errors | `assert` messages, line and column, and the call chain are reported as usual | `@native("fast")` is for code that is known to be correct and must be as fast as possible: integer overflow and index checks are removed, and floating-point reassociation and FMA are allowed, so sums may differ in the last digits (like C with `-O3 -ffast-math`). Use it after the program works with `@native`. `@native("fma")` keeps every check and the order of every operation, and only allows `a * b + c` to become one fused multiply-add instruction (one rounding instead of two). That is what gcc and clang do for C by default, so it is the like-for-like setting against C. Results differ from strict IEEE in the last bits (usually they are slightly more accurate). On a chain of dependent arithmetic it is much faster: the ODE benchmark runs in 288 ms instead of 440 ms on a Xeon 8573C, against 387 ms for gcc -O3 (`benchmarks/heavy/fma.apsi`). `@native("strict")` turns "cannot compile" into an error instead of a note — useful in tests and CI. ## Tensors - **Element loops** compile to direct memory access: `a[i, j]` on a contiguous tensor is one address computation. Tensors passed in are used in place (no copy); writes are visible to the caller, as in Apsi. - **Views:** `m[i]` (a row) and `a[lo:hi]` are views of the same memory, without copying. - **Fused expressions:** `sqrt(a * a + b * b) + 0.5 * a` is computed in one pass with no temporary tensors. Shapes broadcast like NumPy (`m * v` with `m: (6, 5)` and `v: (5,)`); incompatible shapes are an error. - **Reductions fuse too:** `sum(a * b)`, `mean`, `min`, `max`, `argmin`, `argmax`, `prod`, `dot`, `norm` read their input once without building it. - **All cores automatically:** fused expressions and `sum`/`mean`/`prod`/`min`/`max` over 131,072 elements or more are split into the same fixed 256 chunks as `parallel for` and run on every core, without any change to the code (`sum(sqrt(a*a + b*b))` over 4M elements: 11.3 ms on one core, 5.2 ms on two). Sums are combined chunk by chunk in a fixed order, so the result is identical for every thread count; it can differ in the last bits from a strictly sequential sum, as NumPy's pairwise sums do. - **New tensors:** `zeros(n, m)`, `ones`, `full`, `zeros_like`, results of tensor expressions. Tensors created inside a loop iteration are freed at the end of the iteration when they do not outlive it; a tensor that is returned becomes a normal Apsi tensor with no copy. ## Units Units are checked when a function is compiled and then erased: the machine code works on plain SI numbers, so physical units cost nothing at run time, and the result gets its unit back. ```apsi @native fn kinetic(m: Mass, v: Velocity) -> Energy = 0.5 * m * v ** 2 print(kinetic(2kg, 3[m/s])) # 9.0 J @native fn wrong(x: Length, t: Time) = x + t # UnitError: cannot add Length and Time (at compile time) ``` Arguments are checked against the parameter annotations on every call, so `kinetic(2kg, 3s)` is still a `UnitError`. Calls from unit-erased code (inside `evolve`) use a separate specialisation without units. ## `parallel for` ```apsi @native fn mean_and_max(x): var total = 0.0 var biggest = -inf parallel for i in 0..len(x): total += x[i] biggest = max(biggest, x[i]) return total / len(x), biggest ``` - Iterations run on a pool of threads, one per core (`APSI_THREADS=n`, `native_threads(n)` or `apsi run --threads n` to change). The dispatcher and the worker loop are machine code: workers stay awake for a few milliseconds after a loop, spinning on a shared counter with the CPU's `pause` instruction, so back-to-back parallel loops start in about **5 µs** with no operating-system call (Python's thread pool needed ~70 µs). Idle workers then sleep and use no CPU (`APSI_SPIN` sets the spin length). Threads claim several consecutive chunks per atomic operation, so cores do not fight over one cache line, and every field threads share has its own cache line. - Reductions (`+=`, `-=`, `*=`, `x = min(x, …)`, `x = max(x, …)`) use private copies that are combined in a fixed order. The work is always split into the same 256 chunks, so **the result is bit-identical for any number of threads and on every machine**. - Outer variables that are not reductions are read-only inside the loop; a loop that assigns them, or contains `return`, `break` or `print`, runs sequentially, and the note says why. - A `parallel for` inside another one runs sequentially in the thread that reaches it. - Outside native code `parallel for` is an ordinary `for`, so programs run unchanged on every engine. ## `@gpu` ```apsi @gpu fn saxpy(a: Float, x, y): parallel for i in 0..len(x): y[i] = a * x[i] + y[i] @gpu fn sum_of_squares(x) -> Float: var s = 0.0 parallel for i in 0..len(x): s += x[i] * x[i] return s ``` A `@gpu` function runs its `parallel for` with one GPU thread per iteration. - **Requirements:** an NVIDIA GPU with compute capability 7.5 or newer (GTX 1660 / RTX 20xx and every newer generation) and the normal NVIDIA display driver, on Windows 10/11 or Linux. No CUDA toolkit: Apsi generates PTX itself (LLVM NVPTX backend, `sm_75`), and the driver compiles it for the installed GPU and caches it. - **Several GPUs:** the iterations are split evenly across all GPUs (choose them with `APSI_GPUS=0,1`). When every tensor write is to row `i` of the loop (`out[i] = …`, `out[i, j] = …`), each GPU copies back only its own rows; otherwise one GPU is used. Reductions from all GPUs are added together. - **Shape of a GPU function:** optional `let`/`var` lines, one `parallel for` over a range, then optionally `return` of a `+=` reduction variable. The loop body may use arithmetic, comparisons, `if`/`while`/`for`, tensor element reads and writes, `sqrt abs floor ceil min max where clip round int float` and functions that follow the same rules. `exp`, `log`, trigonometric functions, `**` with exponents other than 1 and 2 (any integer up to 16 with `@gpu("fast")`), Float `//` and `%`, `print` and creating tensors are not available inside GPU kernels yet. - **Always runs:** without a usable GPU, or for a function outside these rules, the same function runs as native code on all CPU cores, with the same results; one note says why. - **One thread per element:** when the body of the `parallel for` over rows is a single inner loop whose iterations are independent (`c[i, j] = …`), the two loops become one loop over rows × columns, so a 2048 × 2048 result gets 4 million GPU threads instead of 2,048. Nothing to write differently. - **CPU or GPU, chosen per call size:** a launch of fewer than 4,096 iterations runs on the CPU (the GPU's fixed costs would dominate). For larger sizes the choice is measured per size class (powers of two): the first GPU call is a warm-up (it loads the kernel and is not counted), then one GPU call and one CPU call are timed, and later calls use the faster side. Every 64 calls the slower side is timed once more, so a choice made while the machine was busy does not stick. Each call runs exactly once, so side effects never repeat. `@gpu("strict")` or `APSI_GPU_AUTO=0` always uses the GPU; `APSI_GPU_MIN=n` changes the threshold. - **Matrix products and Tensor Cores:** a `@gpu` function whose loop nest is a matrix product runs as one cuBLAS GEMM when cuBLAS is installed (`pip install nvidia-cublas-cu12`): Tensor Cores with f32 accumulation for f16 and bf16 tensors, SGEMM / DGEMM for f32 / f64. See [Number formats](/docs/2.4.4/numbers). - **Number formats:** f64, f32, f16, bf16, FP8 (E4M3, E5M2) and int8 tensors; 16- and 8-bit values are computed in f32 and rounded once when stored ([Number formats](/docs/2.4.4/numbers)). `var acc: f32 = 0.0` keeps an accumulator in f32, which consumer GPUs run up to 64× faster than f64. - **Data movement:** ordinary tensor arguments are copied to GPU memory for each call and written tensors are copied back. `to_gpu(x)` keeps a tensor in GPU memory instead: kernels read and write it in place, with no copy per call, and `to_host(g)` brings the values back when you need them. Without a GPU, `to_gpu` returns the tensor itself, so the program runs everywhere. - **Precision:** IEEE arithmetic; `sqrt` is correctly rounded, as on the CPU. - **Status:** PTX generation, the kernel rules and the CPU path are covered by the test suite. The launch path uses the CUDA driver API (`nvcuda.dll` / `libcuda.so.1`) and needs a GPU to run, so it is not exercised by continuous integration on GPU-less machines. `apsi native info` and `gpu_info()` show the GPUs Apsi can use; `apsi doctor` reports them too. For training neural networks on the GPU, the PyTorch backend (`fit(…, backend="torch")`, see [`BACKENDS.md`](/docs/2.4.4/backends)) remains the right tool: it brings cuDNN and fused training kernels. `@gpu` is for your own loops and kernels, including matrix products on Tensor Cores. **Other GPU APIs.** `@gpu` targets NVIDIA GPUs through CUDA. Vulkan, DirectX 12 and Metal compute backends (which would add AMD, Intel and Apple GPUs) are planned; until then those GPUs are used through the PyTorch backend (ROCm, XPU, MPS). ## Tools and settings | Command / setting | Effect | |---|---| | `apsi native info` | CPU model, SIMD features, threads, GPUs and the cache location (`--json` for scripts) | | `apsi native explain app.apsi` | runs the program and lists each native function: compiled for which argument types, or why it runs on the bytecode engine (`--all` tries every function) | | `apsi native ir app.apsi f` / `asm` | the optimised LLVM IR or the machine code of function `f` | | `apsi native clear-cache` | empty the machine-code cache | | `apsi run --native app.apsi` | try every function natively (silent fallback), as if each had `@native` | | `apsi run --threads 8 app.apsi` | threads for `parallel for` | | `native_info()`, `gpu_info()`, `native_threads(n)` | the same from inside a program | | `APSI_THREADS=n` | default thread count | | `APSI_NATIVE=auto` | same as `--native` | | `APSI_NATIVE_QUIET=1` | hide "runs on the bytecode engine" notes | | `APSI_NO_CACHE=1` | do not read or write the compile and machine-code caches | | `APSI_NO_GPU=1` / `APSI_GPUS=0,2` | ignore GPUs / use only these GPUs | The native compiler needs `llvmlite` (installed with Apsi; it contains LLVM, nothing else to install). Without it, `@native` functions run on the bytecode engine and `apsi native info` says why. ## Limits - Native `Int` is 64-bit; bigger integers stay on the bytecode engine. - Functions with shape parameters (`fn f[N](...)`) and parameters typed with structs run on the bytecode engine. - Several Python threads calling native functions at the same moment share one error slot; run parallel work with `parallel for` instead of Python threads. - The playground (WebAssembly) has no native compiler; `@native` functions run on the bytecode engine there. # Maximum performance — what to do, and why it matters Apsi programs are correct by default. Being **fast** as well depends on a handful of habits. Each one below says what to do, **why** (what happens inside Apsi), and how big the difference is. They are not style preferences: skipping them is the difference between C-like speed and Python-like speed — often **100 to 3,500 times**. ## The short version 1. Put `@native` on the functions that do the heavy loops. 2. Give those functions simple, stable types: numbers, tensors, structs of numbers — not mixed lists. 3. Work on tensors in place, inside the native function, instead of building Python lists. 4. Use `parallel for` when iterations are independent. 5. Keep units at the edges; they cost nothing inside `@native`. 6. Measure with `apsi bench` and read `apsi native explain` when something stays slow. ## 1. Mark hot functions `@native` **Do:** `@native` above functions with loops over numbers or tensors. **Why:** without it, every operation goes through the bytecode engine, which checks types and units on each step. With it, Apsi compiles the function through LLVM into machine code for your exact CPU (AVX2, AVX-512, FMA), specialised for the argument types it receives; the code is cached on disk, so later runs start instantly. **Difference:** the eight [benchmarks](/benchmarks) run about 500× faster than Apsi 1.10.0 (12× to 2,400× per program) and land next to C, C++ and Rust. **If it does not compile** (an unsupported construct), Apsi falls back to the bytecode engine and prints one note with the line — the program keeps working. `@native("strict")` turns that into an error so you notice; `apsi native explain file.apsi` lists every decision. ## 2. Keep types simple and stable inside hot code **Do:** pass numbers, tensors (`zeros(n)`, `tensor(...)`) and structs of those. Keep one type per variable. **Why:** the native compiler infers a machine type for every variable. `var s = 0` that later becomes a float, or a list holding numbers *and* strings, cannot be given one machine type, so that function stays on the bytecode engine. **Difference:** all-or-nothing for that function — typically 50–500×. ## 3. Prefer tensors to lists in numeric loops **Do:** `let x = zeros(n)` and `x[i] = …` instead of `xs.append(…)` on a list. **Why:** a tensor is one contiguous block of memory of one element type; the compiler reads and writes it directly, removes bounds checks it can prove safe, and vectorises the loop. A list holds separate objects. **Difference:** 10–100× in tight loops, plus far less memory. ## 4. Use `parallel for` for independent iterations **Do:** `parallel for i in 0..n:` when iteration `i` does not read what iteration `j` writes. **Why:** the iterations are split across all CPU cores. Sums, products, `min` and `max` of outer variables are combined safely and give bit-identical results on any number of cores; a loop that writes other outer variables runs sequentially with a note explaining why — so you cannot get a wrong answer silently. **Difference:** close to the number of cores (8 cores → about 7×). ## 5. Units: declare them at the boundaries **Do:** annotate the function's parameters and result (`fn step(x: Length, dt: Time) -> Velocity`). **Why:** units are checked when the function is compiled and once per call; the machine code itself works on plain SI numbers. You keep the safety and pay nothing in the inner loop. **Difference:** zero cost — but skipping units loses the checks that catch a whole class of physics bugs. ## 6. Choose the number format deliberately **Do:** `f64` (default) for physics; `f32` or `bf16` for large AI models (`@precision(f32)`, `dtype=f32`). **Why:** half the bytes per number means half the memory traffic; most AI workloads are memory-bound. **Difference:** up to 2× for memory-bound work, and models that fit in memory at all. See [number formats](/docs/2.4.4/performance). ## 7. Measure instead of guessing ```bash apsi bench app.apsi # repeated timed runs apsi native explain app.apsi # which functions are native, and why not apsi native asm app.apsi FN # the machine code, if you want to see it python3 benchmarks/heavy/run.py # compare with C, C++, Rust, Go and Mojo on your machine — live table ``` **Why:** the slow part is rarely where it seems. Two minutes with `apsi bench` saves hours of rewriting the wrong function. ## Checklist before you publish a library - [ ] hot functions `@native`, and `apsi native explain` shows them compiled - [ ] no lists of mixed types in hot paths - [ ] units on public functions - [ ] tests (`test "…":` blocks) pass with `apsi test` - [ ] `apsi bench` numbers noted in the README, so users know what to expect # Performance: CPU, GPU, RAM, SSD and number formats ## Number formats (`use precision`) | Format | Bits | Largest value | Used for | |---|---|---|---| | `f64` | 64 | 1.8e308 | physics that needs the highest accuracy (the default) | | `f32` | 32 | 3.4e38 | general AI, large simulations | | `f16` | 16 | 65504 | inference, mixed precision | | `bf16` | 16 | 3.4e38 | AI training (same range as f32) | | `fp8_e4m3` (`fp8`) | 8 | 448 | weights and activations for inference/training (as on Hopper/Blackwell GPUs) | | `fp8_e5m2` (`bf8`) | 8 | 57344 | gradients | | `fp6_e3m2`, `fp6_e2m3` | 6 | 28 / 7.5 | OCP MX (4 values packed in 3 bytes) | | `fp4_e2m1` (`fp4`) | 4 | 6 | highly compressed weights (2 values per byte) | | `int8` | 8 | 127·scale | classic quantization | | `int4` | 4 | 7·scale | compressed LLM weights | | `uint8` | 8 | — | image/signal data (with a zero point) | - **Bit-exact with the standards**: every conversion is tested against `ml_dtypes` (the reference library of JAX/TensorFlow) on 20,000 random values with **no differences** — round-to-nearest-even, subnormals and saturation included - `quantize(x, fmt, axis=)` stores real-size codes (FP4 uses 1/16 of the memory of f64); `dequantize(q)` converts back - `round_to(x, fmt)` rounds while staying a normal tensor, with a straight-through gradient, for **quantization-aware training** - `qmatmul(a, b)`: matrix multiply with quantized operands, accumulated in FP32 (the same arithmetic as tensor cores) - `@precision(bf16)`, `@precision(fp8_e4m3)` …: round the tensors going into and out of a function - `Tensor[N, bf16]` in a type annotation: rounds the value when the function is called - `quant_error(x, fmt)`, `format_info(fmt)`, `nbytes(x)` **About speed:** f64/f32/f16 use the CPU's instructions directly. In the current runtime BF16/FP8/FP6/FP4/INT4 are stored as real-size codes (saving RAM, SSD and bandwidth in proportion to the bits) and decoded to FP32 for computing. Results match the hardware exactly, but the speed of FP8/FP4 tensor cores needs the native GPU backend of a later phase (see `ROADMAP.md`). Neural networks already train on GPUs in fp32, tf32, bf16 and fp16 through the PyTorch backend (below). ## Native code (`@native`, `parallel for`, `@gpu`) The fastest way to run loops and numeric functions: machine code through LLVM for the exact CPU, all cores with `parallel for`, NVIDIA GPUs with `@gpu`. On par with C, C++ and Rust, and 100–3,500× faster than bytecode on loops. Benchmarks and the full reference: [`NATIVE.md`](/docs/2.4.4/native). ## CPU - `@native` functions run as machine code (AVX2/AVX-512 vectorised); `parallel for` uses every core — see above - tensor math runs on multi-threaded BLAS (the OpenBLAS/MKL that comes with numpy) on every core automatically - other loops and functions are compiled to bytecode, 5–20× faster than the tree-walker - `use parallel` - `spawn(f, args…)`, then `.wait()`, `wait_all`, `wait_any` - `channel(cap)` (send/recv/close, iterate with for), `lock()`, `atomic()` - `parallel_for(items, f)`, `parallel_reduce(items, f, combine, init)`, `pmap(f, xs)` - `set_threads(n)` limits the BLAS and pool threads (uses threadpoolctl when installed) - `cpu_count()`, `bench(f)` - reductions over short axes (such as xyz coordinates) use an unrolled kernel, about 1.5× faster than plain numpy in N-body work ## GPU (`use gpu`) - your own loops on the GPU: `@gpu` with a `parallel for` — no CuPy or CUDA toolkit needed, several GPUs at once ([`NATIVE.md`](/docs/2.4.4/native#gpu)) - `device()` returns `"cuda:0"` when an NVIDIA GPU with compute capability ≥ 7.5 (GTX 1660 or newer) and CuPy are present, otherwise `"cpu"` - `to_device(x)`, `to_gpu(x)`, `to_cpu(x)`, `gpu_info()` (name, memory, number of SMs) - GPU tensors work with every operator and with autodiff (through numpy's array-function protocol) - install: `apsi pkg install gpu` (or `pip install cupy-cuda12x`), then check with `apsi doctor` ## PyTorch backend (`use layers`) Models built with `use layers` can be trained and run by PyTorch instead of NumPy — on the CPU or on NVIDIA CUDA, AMD ROCm, Apple MPS and Intel XPU GPUs: - `fit(…, backend="torch", device="auto", precision="bf16", compile=true)`; `predict(…, backend="torch")` - precisions `fp64` (matches NumPy to ~1e-15), `fp32`, `tf32` (tensor-core matmuls), `bf16` and `fp16` mixed precision with automatic loss scaling - data stays on the device; fused attention (`scaled_dot_product_attention`), cuDNN LSTMs, parallel `index_add`/`scatter_reduce` for graph layers, one host synchronisation per epoch - `torch_module(model, params)` for DDP/FSDP multi-GPU training, ONNX export and other PyTorch tools - `check_torch()` verifies every layer against NumPy on your machine Needs PyTorch (`apsi pkg install torch-cuda` for NVIDIA GPUs, `apsi pkg install torch` for the CPU) and `--allow-python`. Full guide: [`BACKENDS.md`](/docs/2.4.4/backends). ## RAM and SSD (`use data`) - **memory-mapped tensors** - `open_tensor("x.npy", shape, f32)` creates a file on SSD that behaves like a normal tensor — it may be larger than RAM - `load_tensor(path, mmap=true)` opens a file instantly without loading it into RAM - **streaming pipelines:** `from_tensors(X, y, batch=512, shuffle=true) |> map(f, workers=8) |> prefetch(4)` - batches are sliced vectorized and indices are read in order, so SSD/mmap reads stay fast - `map` runs in parallel on a worker pool and keeps the order - `prefetch` prepares data ahead of time on a separate thread, overlapping with computation - measured with `examples/25_data_pipeline.apsi`: **about 2 million rows per second** (4,000 batches × 512 rows) on 2 vCPUs - `cache(dir)` stores a pipeline's output on SSD as shards, so the next pass reads from disk without recomputing; `cache()` without a folder keeps it in RAM - `@disk_cache("dir")` remembers results of expensive functions on disk, across runs - **every cache file is signed with HMAC-SHA256** using a per-user secret key (`~/.apsi/cache.key`); modified files are never loaded - other sources: `from_csv` (read in chunks), `from_npy`, `from_files("data/**/*.png")`, `from_generator`, `from_range`, `from_list` - other steps: `filter`, `shuffle(buffer)`, `batch`, `unbatch`, `take`, `skip`, `repeat`, `throughput(ds)` ## Start-up: compile cache and executables - a program file is parsed once per process (the Guard scan, the checker, imports and the run share the result) and kept in the signed on-disk compile cache, so an unchanged file is not parsed again on the next run - `apsi compile PATHS` compiles and checks programs ahead of time; `apsi compile --runtime` compiles Apsi itself - `apsi build app.apsi` makes an executable that carries the parsed program and Apsi as bytecode; it starts without parsing or compiling anything (measured: 0.48 s for a small program, most of it loading numpy) - details: the *Executables and the compile cache* section of [`LANGUAGE.md`](/docs/2.4.4/language) ## Measuring - `apsi bench app.apsi -n 10` — best / median / mean / worst over several runs - `apsi run app.apsi --time` — wall time of one run - `:time CODE` and `:bench CODE` inside the REPL - `@timed` on a function prints how long every call takes ## Matrix products written as loops Write the matrix product the way the textbook does — Apsi runs it with the tuned BLAS kernel (the one behind NumPy's `@`), in the compiled engine and in `@native` machine code alike: ```apsi fn matmul_into(a, b, c): for i in 0..a.shape[0]: for k in 0..a.shape[1]: let aik = a[i, k] for j in 0..b.shape[1]: c[i, j] += aik * b[k, j] ``` | n (float64; AMD EPYC 7763 GitHub Actions runner (4 vCPU); best of 3) | same loops in C, `-O3 -march=native` (1 core) | Apsi loops (BLAS, all cores) | | |---:|---:|---:|---| | 256 | 2.28 ms | 0.55 ms | 4.1× faster | | 512 | 18.85 ms | 3.40 ms | 5.5× faster | | 1024 | 155.2 ms | 24.27 ms | 6.4× faster | | 2048 | 2,111 ms | 192.4 ms | **11.0× faster** (89.3 GFLOP/s) | Details and how to reproduce: [`benchmarks/matmul`](../benchmarks/matmul/README.md). **What is recognised.** Three nested counting loops (`0..X` or `range(X)`, the outer one may be `parallel for`) whose innermost statement is * `c[i, j] += a[i, k] * b[k, j]` in any loop order, with the factors in either order, or with `a[i, k]` hoisted into `let t = a[i, k]` between the `i`/`k` loops and the `j` loop, or * the dot-product form: `var s = 0.0`, then `for k: s += a[i, k] * b[k, j]`, then `c[i, j] = s`. The loop bounds may be any expressions without side effects that do not depend on the loop variables (`n`, `a.shape[0]`, `len(b)`, `m - 1` …); sub-blocks (`0..m` smaller than the arrays) are supported. **When the loops run instead.** Only when the result is exactly what the fast path computes: `a`, `b` and `c` must be 2-D arrays of float64 (or all float32) and `c` must not share memory with `a` or `b`. Integer arrays, arrays with units carried at run time, bounds beyond the arrays (the loops then raise the same index error they always did), `c` overlapping an input — all run the loops as written, so behaviour never changes. The only difference you can see is the one every optimised BLAS has: products are summed in a different order, so results can differ from the loops in the last bits (relative differences around 1e-16 for float64). `APSI_IDIOMS=0` turns the recognition off, for example to time the loops themselves. ## Outer loops four at a time (exact SIMD for reductions) ```apsi for i in 0..n: var fx = 0.0 for j in 0..n: fx += (x[j] - x[i]) * m[j] / d(i, j) ax[i] = fx ``` The inner loop adds into `fx` one term after another. Vectorising it along `j` would change the order of the additions — and so the result — which strict IEEE arithmetic forbids. The outer iterations are independent, though, so `@native` code runs **four values of `i` at once**: four accumulators, each receiving exactly the same additions in exactly the same order as before. The four lanes are identical instruction streams on different data, which LLVM packs into SIMD registers. Results are bit-for-bit identical to the plain loop (the test suite checks this); only the speed changes. C compilers call this outer-loop vectorisation. It applies when the outer loop counts by one, its body is straight-line code around one inner counting loop that accumulates into variables declared in the outer body, tensors written in the nest are written only at the outer index (`ax[i]`) — or only after the inner loop, like `sums[cluster] += x[i]`, which keeps its original order — and only built-in math functions are called. Examples: n-body forces, k-means assignment, matrix-vector products, distance matrices. `APSI_JAM=0` turns it off. **The lanes are emitted as SIMD vectors directly.** Leaving the packing to LLVM's SLP vectoriser worked on some processors and not on others: with LLVM 22 on AVX2 machines (AMD Zen 2/3, Intel without AVX-512) it grouped the twelve n-body accumulators into one 12-wide vector and spent its time shuffling, so the n-body benchmark took 127.5 ms on an EPYC 7763; with the vectors written by Apsi it takes 46.0 ms (gcc -O3: 44.2 ms). Now, when the inner loop is straight-line Float code (`+ - * /`, unary minus, `sqrt`, `abs`) on lane variables and values shared by all lanes, `@native` writes the four lanes as `<4 x double>` operations itself: every lane still performs exactly its own operations in its own order, so the results stay bit-for-bit identical (`tests/test_jam.py` checks values and whether each result is a Python float or a NumPy scalar), and the speed no longer depends on the processor model. Loops with `if` (k-means) keep the scalar lanes. `APSI_JAM_VEC=0` turns the vector form off, `APSI_JAM_LANES=n` changes the number of lanes (default 4). # Number formats: f64, f32, f16, bf16, FP8 and int8 Apsi tensors can hold six floating-point formats and the usual integer ones. All of them work in ordinary code, in `@native` machine code and in `@gpu` kernels, with one rule that keeps results trustworthy: **small formats are a way to store numbers, not to compute with them.** Values are read into 32-bit registers, all arithmetic happens in f32 (or f64), and results are rounded once, correctly, when they are stored. | Format | Name in Apsi | Bits (sign / exponent / mantissa) | Largest value | Decimal digits | Typical use | |---|---|---|---:|---:|---| | IEEE double | `f64` | 1 / 11 / 52 | 1.8 × 10³⁰⁸ | 15–16 | science, finance, anything that must be exact to the last digit | | IEEE single | `f32` | 1 / 8 / 23 | 3.4 × 10³⁸ | 7 | graphics, signal processing, most of machine learning | | IEEE half | `f16` | 1 / 5 / 10 | 65,504 | 3 | inference, images, half the memory of f32 | | bfloat16 | `bf16` | 1 / 8 / 7 | 3.4 × 10³⁸ | 2–3 | training: f32's range in 16 bits | | FP8 E4M3 | `fp8_e4m3` (`fp8`) | 1 / 4 / 3 | 448 | 1–2 | weights and activations of large models | | FP8 E5M2 | `fp8_e5m2` (`bf8`) | 1 / 5 / 2 | 57,344 | 1 | gradients (more range, less precision) | | integers | `int8`, `i32`, `i64`, `uint8`, `bool` | | | exact | quantised weights, labels, images, masks | ## Making tensors ```apsi let w = zeros(4096, 4096, dtype=bf16) # 32 MB instead of 128 MB in f64 let h = randn(1024, 1024).astype("float16") let q = zeros(1000, dtype=fp8_e4m3) var acc: f32 = 0.0 # a scalar kept in f32 (also f16, bf16, f64) ``` `bf16` and the FP8 formats come from the `ml_dtypes` package (the format library JAX and TensorFlow use), which is installed with Apsi. They behave like any NumPy dtype: slicing, `astype`, saving and loading all work. ## In `@native` and `@gpu` code ```apsi @native fn scale(x, y, a: Float): # x, y may be f64, f32, f16, bf16, FP8 or int8 tensors for i in 0..len(x): y[i] = x[i] * a ``` | | Reading `x[i]` | Arithmetic | Writing `y[i] = v` | |---|---|---|---| | f64 | as is | f64 | as is | | f32 | as is | f32 | as is | | f16, bf16, FP8 | widened to f32 (exact) | f32 | rounded to nearest, ties to even | | int8 … i64 | widened to Int | Int (checked for overflow) | checked: a value that does not fit is an error | Why this design: * **Accuracy.** Adding 100,000 copies of 0.1 in f16 arithmetic stops at 256, because 256 + 0.1 rounds back to 256. With the data in f16 and the sum in f32 the total is 9,998.4; with a `Float` (f64) accumulator it is 9,997.55859375 — exactly 100,000 × the f16 value of 0.1. Products of two f16 or bf16 numbers are exact in f32. * **Speed.** Most loops are limited by memory bandwidth, not arithmetic. Reading 2 bytes instead of 8 per element moves 4× less data; FP8 moves 8× less. * **The same answer everywhere.** The conversions are written with integer operations, so every CPU (with or without F16C) and every GPU produces the same bits. They are checked against NumPy and `ml_dtypes` for every one of the 65,536 16-bit patterns and all 256 FP8 patterns. Rounding details: conversions to a smaller format round to the nearest value, ties to even. NaN stays NaN. A value too large for f16, bf16 or FP8 E5M2 becomes infinity; FP8 E4M3 has no infinity, so it becomes NaN (the OCP FP8 standard and `ml_dtypes` do the same). Use `clip(x, -448, 448)` first if you want saturation instead. ## Matrix products and Tensor Cores A matrix product written as loops (see [Performance](/docs/2.4.4/performance#matrix-products-written-as-loops)) uses the fastest kernel available for its format: | Format | CPU | NVIDIA GPU with cuBLAS | NVIDIA GPU without cuBLAS | |---|---|---|---| | f64 | BLAS DGEMM | cuBLAS DGEMM | Apsi's kernel | | f32 | BLAS SGEMM | cuBLAS SGEMM (TF32 only with `APSI_TF32=1`) | Apsi's kernel | | f16, bf16 | Apsi's loops (f32 math) | **Tensor Cores**, f32 accumulation | Apsi's kernel (f32 math) | | FP8, int8 | Apsi's loops | Apsi's kernel | Apsi's kernel | cuBLAS comes with the CUDA Toolkit, or `pip install nvidia-cublas-cu12` (no toolkit needed). `apsi native info` says whether Apsi found it. ## Smaller still: FP6, FP4, int4 and others Formats below 8 bits (`fp6_e3m2`, `fp6_e2m3`, `fp4_e2m1`, `int4`, `nf4` …) are available for quantisation: `quantize(x, fp4_e2m1)` packs a tensor, `dequantize` unpacks it, `@precision(fp4_e2m1)` runs a function as if its values had that format, and `load_safetensors(path, dtype=int4)` loads weights into it. See [AI](/docs/2.4.4/ai) and `use precision`. ## Choosing a format * **Results you publish or bill for:** f64. It costs little on a CPU. * **Large arrays where 7 digits are plenty:** f32 — half the memory of f64, and far faster on consumer GPUs (an RTX 3060 does f64 at 1/64 of its f32 speed). * **Model weights and activations:** bf16 for training, f16 or bf16 for inference, FP8 E4M3 when memory is tight. * **Accumulators:** always f32 or f64 (`var acc: f32 = 0.0`), whatever the data format. # Apsi security model Apsi assumes any program might be hostile, including one you downloaded, one an AI wrote, or a dependency of either. The defences are layered. Each layer still holds if another one is turned off. | Layer | What it stops | Where | |---|---|---| | 1. Capability permissions | file, network, environment, Python and native-code access that was not granted | every built-in function | | 2. Guarded locations | changes to Apsi's own settings, the runtime, system folders, shell start-up files; reads of credentials | `check_path` | | 3. Network guard | SSRF to cloud metadata and link-local addresses, DNS tricks, redirect hops to non-allowed hosts, oversized responses | `http_get`, `http_post_json` | | 4. Apsi Guard static scan | malware patterns, checked before `apsi run` / `apsi serve` and on demand with `apsi scan` | `apsi/guard.py` | | 5. Behaviour monitor | ransomware and wipers: mass overwrite or delete of existing files, decoy files | runtime | | 6. Python / native-code control | sandbox escape through `subprocess`, `os`, `ctypes`, ...; native libraries planted in temp or Downloads; library swapping | `pyimport`, `cload` | | 7. Resource limits | memory exhaustion and runaway programs | `--max-memory`, `--timeout` | | 8. Integrity | a modified runtime or installer | `apsi verify`, installer SHA-256 check | ## 1. Permissions and profiles ``` apsi run app.apsi # profile from `apsi config` (default: standard) apsi run app.apsi --sandbox # nothing allowed: pure computation apsi run app.apsi --profile strict # files only inside the program's folder apsi run app.apsi --allow-read=data --allow-write=out apsi run app.apsi --allow-net=api.example.com:443,*.example.org apsi run app.apsi --allow-env=HOME,API_TOKEN apsi run app.apsi --allow-python # numpy/torch/... but NOT subprocess, os, ctypes, socket apsi run app.apsi --allow-python=all # every Python module (full access: trusted code only) apsi run app.apsi --allow-ffi=vecmath.so@ # native library pinned by hash apsi config --profile strict # change the default for this computer apsi audit app.apsi # which capabilities a program uses + the minimal flags ``` | Profile | read | write | net | env | python | ffi | |---|---|---|---|---|---|---| | sandbox | – | – | – | – | – | – | | strict | program folder | program folder | – (private IPs blocked even with `--allow-net`) | – | – | – | | standard | anywhere* | anywhere* | – | – | – | – | | developer | anywhere* | anywhere* | any† | any | all | any | \* Except the guarded locations below. † Cloud metadata and link-local addresses stay blocked. ## 2. Guarded locations - **Never writable, under any profile:** `~/.apsi` (so a program cannot raise its own permissions), the Apsi runtime and Python installation, system folders (`/etc`, `/usr`, `C:\Windows`, `Program Files`), shell start-up files (`.bashrc`, `.profile`, `.zshrc`), `~/.local/bin`, the Windows Startup folder and XDG autostart. You can add more folders with `APSI_PROTECT=dir1;dir2`. - **Credentials need an explicit grant:** a blanket `--allow-read` does not cover `~/.ssh`, `~/.aws`, `~/.azure`, `~/.kube`, `~/.docker`, `~/.gnupg`, `.netrc`, `.git-credentials`, browser profiles (Chrome, Edge, Firefox), Windows Credential Manager folders or `/etc/shadow`. Grant one with `--allow-read=~/.ssh`. - Paths are resolved with `realpath` before every check, so a symlink inside an allowed folder cannot point outside it. NUL bytes, `\\?\` device paths, reserved names (`CON`, `NUL`, `COM1`, ...) and NTFS alternate data streams (`file.txt:hidden`) are refused. ## 3. Network guard - Only `http://` and `https://`, and no credentials inside URLs. - The host is resolved, and every address is checked. Cloud metadata (`169.254.169.254`), link-local, multicast and reserved addresses are blocked unless the host is listed exactly in `--allow-net`. The strict profile also blocks private and loopback addresses for wildcard grants. - Every redirect hop is checked against the same policy, with at most 5 redirects. - Responses are capped at 64 MB (`APSI_HTTP_MAX_BYTES`). ## 4. Apsi Guard: static threat scan `apsi run` and `apsi serve` scan a program before running it: - **critical** findings always block the run. - **high** findings block under the sandbox and strict profiles and print a warning otherwise. - `--trust` skips the scan for one run. `APSI_SCAN=off`, or `scan_on_run: "off"` in `~/.apsi/config.json`, turns it off. ``` apsi scan downloads/project # .apsi, .py, .dll/.so/.exe, scripts; exit 1 at "high" or worse apsi scan . --fail-on critical # use in CI apsi scan . --av # also Microsoft Defender (MpCmdRun) or ClamAV apsi scan . --json # machine-readable report ``` | Rule | Severity | Detects | |---|---|---| | trojan-source | critical | bidirectional override characters (CVE-2021-42574) | | invisible-char / homoglyph | high | zero-width characters; identifiers using look-alike Cyrillic, Greek or Armenian letters (normal Greek physics names such as `ω`, `π`, `θ` are fine) | | exfiltration | critical | credential locations read in a program that also uses the network | | credential-access | high | SSH keys, cloud credentials, browser `Login Data`/`Cookies`, wallets, Telegram/Discord tokens | | persistence | high | Startup folder, `CurrentVersion\Run`, crontab, systemd, LaunchAgents, `.bashrc`, `LD_PRELOAD` | | sandbox-escape / dynamic-import | high | `pyimport("subprocess")` and other system modules; computed module names | | system-library | high | `cload("kernel32")`, `ntdll`, `advapi32`, `libc`, ... | | download-execute | high | download + write + native or Python execution | | ransomware-pattern | critical | a loop that walks files, reads them, encrypts them and writes them back | | suspicious-endpoint | critical–low | mining pools (`stratum+tcp`), xmrig, Discord/Telegram webhooks, paste sites, `.onion`, metadata endpoints, raw IP addresses | | shell-command | high | `powershell -enc`, `curl … \| sh`, `certutil -urlcache`, `vssadmin delete shadows`, ... | | obfuscation | medium | long base64/hex blobs, high-entropy strings | | python-* | critical–medium | in `.py` companions: `exec(base64…)`, injection APIs, `shell=True`, `pickle.loads` | | disguised-binary, injection-imports | critical / high | executables renamed to documents; PE files importing `WriteProcessMemory` + `CreateRemoteThread` | | blocklisted | critical | SHA-256 listed in `~/.apsi/blocklist.txt` (one hash per line) | | yara:* | high | your YARA rules in `~/.apsi/rules/*.yar` (needs `apsi pkg install yara-python`) | | eicar | critical | the standard antivirus test file, used to check the scanner works | ## 5. Behaviour monitor: ransomware shield The monitor counts writes that modify existing files. If a program changes more than **150** (`guard_max_overwrites`, `APSI_GUARD_MAX_OVERWRITES`) within **20 s** (`guard_window_s`), Apsi stops it. Touching any file whose name starts with `.apsi-decoy` stops a program immediately: put decoy files in folders you care about. Files a program creates itself do not count. ## 6. Python and native code - `--allow-python` allows ordinary libraries. Modules that reach the operating system directly (`subprocess`, `os`, `sys`, `ctypes`, `socket`, `shutil`, `importlib`, `pickle`, `multiprocessing`, `winreg`, ...) need `--allow-python=all`. Harmless helpers (`importlib.util`, `ctypes.util`, `os.path`) are allowed. - Installed Apsi runs Python isolated (`-I`), so a stray `json.py` in your project folder cannot hijack the runtime. - `cload` refuses libraries in world-writable folders, in the temporary folder, and (unless pinned) in Downloads. Pins come from `--allow-ffi=name@sha256` or from `ffi_pins` in the config. A pinned library with a different hash is refused. - **`@native` and `@gpu` code needs no permission and gains none.** The native compiler only accepts computation: numbers, tensors passed in, other Apsi functions and `print`. It cannot open files, use the network, run programs or call Python or C libraries, so machine code produced from a program can do nothing the program could not already do. Index, overflow and division checks are on by default (`@native("fast")` removes them for code you trust; out-of-range accesses are then undefined, as in C). ## 7. Packages `apsi pkg install` installs **binary wheels only** by default, so no package build scripts run on your machine. Use `--allow-source` to change that. Package indexes must use HTTPS, and package names containing shell metacharacters are refused. `apsi pkg lock` writes exact versions with SHA-256 hashes, and `apsi pkg install -r apsi.lock` then requires them. `apsi pkg audit` runs `pip check` and `pip-audit`. ## 8. Integrity and the installer - `apsi verify` compares every runtime file with `MANIFEST.json`. - The setup wizard re-hashes **every payload file** against `payload-files.json`, whose own hash is pinned in `payload.json`, before it copies anything. Extra files, such as a planted DLL, are refused as well. - The wizard runs Electron with context isolation, a sandboxed renderer and no Node.js in the UI. It uses a strict CSP, denies all browser permissions and blocks every non-local request. Each IPC call is checked against its sender, and installer options are rebuilt from a whitelist. The uninstall target comes from the detected installation, never from the UI. - The packaged binary has its Electron fuses flipped: no `ELECTRON_RUN_AS_NODE`, no `NODE_OPTIONS`, no `--inspect`, and app code loads only from the integrity-checked asar. - The install folder is validated: system folders, drive roots, symlinks and non-empty non-Apsi folders are refused. Synced folders such as OneDrive get a warning. - `~/.apsi/config.json` is written with mode 600, and blocked actions are logged to `~/.apsi/audit.log` (rotated at 5 MB). - Code signing: set `CSC_LINK` and `CSC_KEY_PASSWORD` before `npm run dist:win`, and electron-builder signs with SHA-256 and an RFC 3161 timestamp. ## 9. Executables and caches - **`apsi build`** fixes a program's permissions when it is built (`apsi build app.apsi --allow-read`); the executable runs with exactly those (`apsi build` lists them), and an `apsi.toml` next to where it runs cannot add any. A program with check errors is never built. Apsi Guard scans the program every time it starts, as with `apsi run`. - An executable unpacks its program files only below `~/.apsi/apps/NAME-ID`; archive entries that would land outside that folder are ignored. - **The compile cache** (`~/.apsi/cache/compiled`) holds parsed programs as pickles. Every entry is signed with the per-user key `~/.apsi/cache.key` (HMAC-SHA256) and verified before it is read; an entry that was modified or copied from another computer is deleted without being unpickled. `APSI_NO_CACHE=1` turns the cache off, `apsi compile --clear` empties it. - **The machine-code cache** (`~/.apsi/cache/native`) holds object code produced by the native compiler. Every entry is signed with the same per-user key and verified before it is loaded; a modified or foreign entry is deleted and the code is compiled again. The key includes the IR, the CPU model and features, and the LLVM and Apsi versions. `APSI_NO_CACHE=1` turns it off; `apsi native clear-cache` empties it. - **The PyTorch backend** (`fit(…, backend="torch")`, `torch_module`, `check_torch`) runs PyTorch, which is Python code outside Apsi's sandbox, so it needs the `python` permission like `pyimport`; `backend="auto"` stays on NumPy without it. ## Reporting a vulnerability Do not open a public issue. Send the details and a proof of concept to the maintainers' security address on the official site. Fixes are released as `2.0.x` patch versions. # Building AI with Apsi Apsi does not force a ready-made architecture on you. Every network is an ordinary function that takes a "parameter tree" (record / list / struct), and `grad` differentiates with respect to every parameter in it. Whatever architecture you invent, you can write it down directly. ## 1. Build any architecture with `use layers` Every layer is a pair of pure functions — `init(seed)` creates its parameters, `layer(params, x)` applies it — and layers compose into graphs. Models are ordinary values: `grad`, optimizers, `save_params` and `fit` work on them. This section is an overview; **[`LAYERS.md`](/docs/2.4.4/layers) is the complete reference** (every layer with its shapes and parameters, error messages, recipes). ```apsi use layers use nn let model = sequential([linear(2, 64), activation("gelu"), dropout_layer(0.1), linear(64, 3)]) var params = model.init(seed=0) let logits = model(params, X) # model(params, X, train=true) turns dropout on let r = fit(model, params, X, y, loss="cross_entropy", opt=adam(lr=0.01), epochs=50, batch_size=64) summary(model, r.params) # layer table with parameter counts ``` | Kind | Layers | |---|---| | dense | `linear(in, out)`, `dense(in, out, act)`, `bilinear`, `embedding(vocab, d)`, `positional_encoding(len, d, learned)` | | convolution | `conv2d_layer(c_in, c_out, k, stride, padding="same")`, `max_pool(k)`, `avg_pool(k)`, `global_avg_pool()`, `flatten_layer()` | | recurrent | `lstm_layer(in, hidden, return_sequences)`, `gru_layer(…)`, `last_step()` | | attention | `multi_head_attention(d, heads, causal)` (self- or cross-attention), `transformer_block(d, heads, ff, causal)`, `transformer_encoder(d, heads, layers)` | | **graph neural networks** | `gcn(in, out)`, `sage(in, out, aggregate)`, `gat(in, out, heads)`, `gin(in, out)`, `graph_pool("mean"\|"sum"\|"max")` + the graph `graph(edges=…, n=…, undirected, batch)` and the differentiable ops `segment_sum/mean/max/softmax`, `gather` | | normalisation | `layer_norm_layer(d)`, `batch_norm_layer(d)`, `rms_norm_layer(d)`, `dropout_layer(p)`, `scale(d)` | | activations | `activation("relu" \| "gelu" \| "silu" \| "swish" \| "mish" \| "elu" \| "leaky_relu" \| "tanh" \| "sigmoid" \| "softplus" \| "softmax")` or `activation(any_function)` | | composition | `sequential([...])`, `residual(inner, pre_norm)`, `parallel(branches, combine="concat"\|"add"\|"mul"\|"mean"\|"list")`, **`dag(nodes, inputs, output)`** — any directed acyclic graph, several inputs and outputs, `add()`, `multiply()`, `concatenate()`, `lambda_layer(fn)`, `select(i)`, `identity()`, `reshape_layer(...)` | **Any graph of layers** — each node is `[name, layer, input(s)]`: ```apsi let net = dag([ ["wide", linear(2, 3), "x"], ["deep", mlp([2, 32, 32, 3]), "x"], ["both", add(), ["wide", "deep"]], ], inputs=["x"], output="both") ``` **Graph neural networks** — pass the graph when applying or fitting; `mask` limits the loss to labelled nodes: ```apsi let g = graph(edges=[[0, 1], [1, 2], [2, 0]], n=3, undirected=true) let gnn = gat_net(8, 16, classes, heads=4) let r = fit(gnn, gnn.init(seed=0), features, labels, loss="cross_entropy", graph=g, mask=train_mask, epochs=200) ``` **Model zoo** — `mlp(sizes)`, `resnet_mlp(in, out, width, blocks)`, `cnn(channels, classes, image)`, `lstm_classifier(in, hidden, classes)`, `transformer_lm(vocab, d, heads, layers, context)` (GPT-style), `transformer_classifier(vocab, classes, …)`, `autoencoder(in, latent)`, `vae(in, latent)` with `vae_loss`, `gcn_net`, `gat_net`, `gin_net` (graph classification), `siamese(encoder, combine, head)`. **Training** — `fit(model, params, X, y, loss="mse"|"cross_entropy"|"mae"|"bce"|"vae"|fn, opt, epochs, batch_size, clip, schedule, graph, mask, val=(Xv, yv))` returns `record(params, history, val_history)`; `predict`, `summary`, `count_params`. Full example: `examples/28_any_architecture.apsi`. **On GPUs with PyTorch** — the same model, trained by PyTorch on CUDA / ROCm / Apple MPS / Intel XPU with mixed precision and `torch.compile`; the result is a normal Apsi parameter tree again (details: [`BACKENDS.md`](/docs/2.4.4/backends)): ```apsi let r = fit(model, params, X, y, loss="cross_entropy", epochs=20, batch_size=256, backend="torch", device="cuda", precision="bf16", compile=true) # run with --allow-python let m = torch_module(model, r.params) # a real torch.nn.Module: state_dict, DDP, ONNX export check_torch() # every layer compared with NumPy (outputs and gradients) ``` ## 2. Design your own, function by function ```apsi use nn # a home-made architecture: gated residual blocks fn block(p, x): let gate = sigmoid(x @ p.g.W + p.g.b) let h = gelu(layer_norm(x) @ p.f.W + p.f.b) return x + gate * h fn model(p, x): var h = x @ p.inp.W for layer in p.blocks: h = block(layer, h) return h @ p.out.W let params = record(inp=dense(16, 64), blocks=[record(g=dense(64, 64), f=dense(64, 64)) for _ in 0..4], out=dense(64, 1)) let loss, grads = value_and_grad(fn(p) => mse(model(p, X), y))(params) ``` Building blocks: | Area | Available | |---|---| | Ops | `@`, `einsum`, conv2d, max-pool, attention, layer/batch/RMS norm, dropout, embeddings, causal mask, LSTM, GRU | | Activations | relu, gelu, sigmoid, tanh, softplus, softmax, log-softmax — and any function you write | | Losses | mse, cross-entropy, or your own (including physics losses that use higher-order autodiff) | | Optimizers | SGD (momentum), Adam, AdamW, RMSprop, Adagrad, L-BFGS (`minimize`), gradient clipping, LR schedules | | Training | the ready-made `train()` or your own loop (`value_and_grad` + `opt.step`) | | Data | `use data` (streaming, parallel, prefetch, SSD cache) | | Precision | f64/f32/f16/bf16/fp8/fp6/fp4/int8/int4, quantization-aware training (`round_to`) | ## 3. Use less RAM / VRAM | Technique | How | Effect | |---|---|---| | Memory-mapped loading | `load_safetensors(path)` | any file size opens instantly; only the parts you use are read | | Load in a small format | `load_safetensors(path, dtype=int8)` / `fp8_e4m3` / `int4` / `fp4_e2m1` | 4–8× smaller than f32 (16× smaller than f64) | | Compress an existing model | `quantize_model(params, int8)`, then compute with `qlinear(x, W, b)` | example 26: 67.5 KB → 18.6 KB with the same accuracy (99.3 %) | | Train to tolerate compression | `round_to(W, fp8_e4m3)` inside the forward pass | gradients pass through (straight-through estimator) | | Data larger than RAM | `open_tensor` / `load_tensor(mmap=true)` + `from_tensors(batch=…)` | stream from SSD | ## 4. Use pretrained models from other ecosystems Install the library with pip as usual and call it from Apsi (run with `--allow-python`): ```apsi use models let classify = hf_pipeline("sentiment-analysis") # pip install transformers torch print(classify("Apsi is great")) let net = onnx_model("resnet50.onnx") # pip install onnxruntime(-gpu) let logits = net(image_batch) # picks TensorRT / CUDA / DirectML / CPU for you let weights = torch_load("checkpoint.pt") # pip install torch (safe weights_only mode) let hf_weights = load_safetensors("model.safetensors", dtype=int8) # no extra library needed ``` Existing PyTorch/numpy code can be translated with `apsi convert model.py`: `nn.Module` models, losses, optimizers, data loaders and training loops become native Apsi that runs without Python or PyTorch, with the same outputs and gradients as PyTorch for the same weights. `load_torch_weights(model, params, state_dict)` imports pretrained weights (see [`PYTHON.md`](/docs/2.4.4/python#pytorch-programs--native-apsi)). Any other Python library: `pyimport("module")`, then `to_torch` / `from_torch` to move tensors back and forth (after `use layers`, `torch_module(model, params)` turns a whole Apsi model into a `torch.nn.Module`). `apsi pkg install ai` installs ONNX Runtime, PyTorch and Transformers into Apsi's own runtime. ## 5. Serve it as an API ```apsi use web let app = server() @app.post("/predict") fn predict(req) = {"y": model(params, tensor(req.json()["x"]))} app.run(port=8080) ``` Complete examples: `examples/23_web_api.apsi`, `26_model_serving.apsi` and `27_pretrained_models.apsi`. Server security is covered in [`SERVER.md`](/docs/2.4.4/server). ## 6. Save and share models `save_model` writes an Apsi checkpoint (`.apsim`): a standard safetensors file that also keeps units, compressed tensors and the parameter tree, checks a SHA-256 when loading, can be signed, and splits huge models into shards. `save_gguf` writes GGUF for llama.cpp, Ollama and LM Studio, with Q8_0 and Q4_0 bit-identical to ggml. `apsi model convert` converts between safetensors, GGUF and `.apsim`. Full reference: [`MODEL-FILES.md`](/docs/2.4.4/model-files). ```apsi use models save_model("ckpt/model.apsim", params, metadata=record(step=step), sign=true) let params = load_model("ckpt/model.apsim", key=true) save_gguf("model-q8.gguf", params, dtype="q8_0") ``` ## Model file safety - `load_safetensors` never runs code from the file (unlike pickle) and checks that every offset stays inside the file - `load_model` and `load_gguf` check every header size and offset against the file before mapping it, and `load_model` checks the SHA-256 (and the signature when you give a key) - `torch_load` uses `weights_only=True`, so code embedded in a `.pt` file is not executed - the on-disk cache is signed with HMAC # `use layers` — reference `use layers` builds neural networks of **any architecture** out of small, composable layers: plain stacks, residual networks, any directed acyclic graph (several inputs and outputs, shared weights), convolutional and recurrent networks, transformers and graph neural networks. Models are ordinary Apsi values, so `grad`, `value_and_grad`, the optimizers of `use nn`, `save_params` / `load_params` and `fit` all work on them. Every model runs on NumPy (exact float64, always available) or on PyTorch (CPU, CUDA, ROCm, Apple MPS, Intel XPU) — see [`BACKENDS.md`](/docs/2.4.4/backends). - [1. The model contract](#1-the-model-contract) - [2. Parameters](#2-parameters) - [3. Layers](#3-layers) - [4. Composition: sequential, residual, parallel, dag](#4-composition) - [5. Graph neural networks](#5-graph-neural-networks) - [6. The model zoo](#6-the-model-zoo) - [7. Training: fit, predict](#7-training) - [8. Inspection, saving and loading](#8-inspection-saving-and-loading) - [9. Writing your own layer](#9-writing-your-own-layer) - [10. Recipes](#10-recipes) - [11. Errors and how to fix them](#11-errors-and-how-to-fix-them) --- ## 1. The model contract Every layer and every model is a value with two operations: | Operation | Meaning | |---|---| | `model.init(seed=0)` | creates the parameters — a tree of records and lists of tensors. The same seed gives the same parameters on every machine. | | `model(params, x, train=false, graph=…, mask=…)` | applies the model. `train=true` turns on dropout and the VAE's random sampling. `graph` is needed by graph layers, `mask` is an additive attention mask. | ```apsi use layers use nn let model = sequential([linear(4, 32), activation("gelu"), linear(32, 3)]) var params = model.init(seed=0) let logits = model(params, randn(8, 4)) # shape (8, 3) ``` Because the parameters are kept outside the model, a model value can be shared freely (between threads, between training runs, between a server and a trainer) and several parameter sets can be used with the same model. ## 2. Parameters `init` returns a tree whose leaves are tensors: - a layer with weights returns a `record`, e.g. `linear(4, 3).init()` → `record(W=Tensor[4, 3], b=Tensor[3])` - `sequential` and `parallel` return a **list** with one entry per layer - `dag` returns a **record** with one field per node name - layers without weights (activations, pooling, dropout, …) return an empty `record()` Read and change parameters like any other value: `params[0].W`, `params.encoder[2].b`. Every function that works on trees — `tree_map`, `grad`, optimizers, `count_params`, `save_params` — works on these. Initialisation: `linear` uses He initialisation (`init="glorot"` for Glorot/Xavier), convolutions He, attention and GNN weights Glorot, embeddings N(0, 0.02²), LSTM/GRU uniform ±1/√hidden with the LSTM forget-gate bias set to 1. ## 3. Layers Shapes: `B` batch, `T` sequence length, `D` features, `C` channels, `H × W` image size, `N` graph nodes. ### Dense and embeddings | Constructor | Input → output | Parameters | |---|---|---| | `linear(n_in, n_out, bias=true, init="he")` | `[…, n_in]` → `[…, n_out]` | `W[n_in, n_out]`, `b[n_out]` | | `dense(n_in, n_out, act=none)` | linear followed by an activation | as `linear` | | `bilinear(n1, n2, n_out)` | `[a: [B, n1], b: [B, n2]]` → `[B, n_out]`, `aᵀ W_k b + b_k` | `W[n_out, n1, n2]`, `b` | | `embedding(vocab, d)` | integer ids `[B, T]` → `[B, T, d]` | `E[vocab, d]` | | `positional_encoding(max_len, d, learned=false)` | `[B, T, d]` → same, adds position information (sinusoidal, or a learned table) | `P[max_len, d]` when learned | ### Convolution and pooling (images `[B, C, H, W]`) | Constructor | Meaning | |---|---| | `conv2d_layer(c_in, c_out, k=3, stride=1, padding="same")` | 2-D convolution (cross-correlation, as in PyTorch); `padding` is `"same"` (k // 2) or a number. Parameters `W[c_out, c_in, k, k]`, `b[c_out]`. | | `max_pool(k=2)`, `avg_pool(k=2)` | k × k pooling with stride k (an incomplete border is dropped) | | `global_avg_pool()` | `[B, C, H, W]` → `[B, C]` | | `flatten_layer()` | `[B, …]` → `[B, everything else]` | | `reshape_layer(d1, d2, …)` | `[B, …]` → `[B, d1, d2, …]` | ### Sequences | Constructor | Meaning | |---|---| | `lstm_layer(n_in, hidden, return_sequences=true)` | `[B, T, n_in]` → `[B, T, hidden]` (or `[B, hidden]`, the last step, when `return_sequences=false`). Gates i, f, g, o in one fused weight `W[n_in + hidden, 4·hidden]`. | | `gru_layer(n_in, hidden, return_sequences=true)` | GRU with weights `Wzr`, `bzr`, `Wh`, `bh` | | `last_step()` | `[B, T, D]` → `[B, D]` | | `mean_pool(axis=1)` | mean over an axis, e.g. over time `[B, T, D]` → `[B, D]` | ### Attention and transformers | Constructor | Meaning | |---|---| | `multi_head_attention(d, heads=4, causal=false)` | self-attention on `x: [B, T, d]`, or cross-attention on `[query, memory]`. `causal=true` hides future positions; `model(p, x, mask=M)` adds any additive mask `M[T, S]` (use `-1e9` to hide). Parameters `Wq, Wk, Wv, Wo[d, d]`, `bo[d]`. | | `transformer_block(d, heads=4, ff=4·d, causal=false, dropout=0.0, activation="gelu")` | pre-norm block: `x + attention(norm(x))`, then `x + MLP(norm(x))` | | `transformer_encoder(d, heads=4, layers=2, ff, causal, dropout)` | `layers` blocks followed by a final layer norm | ### Normalisation, regularisation, scaling | Constructor | Meaning | |---|---| | `layer_norm_layer(d, eps=1e-5)` | normalises the last axis; `gamma`, `beta` | | `rms_norm_layer(d, eps=1e-6)` | `x / rms(x) · gamma` (as in LLaMA-style models) | | `batch_norm_layer(d, eps=1e-5)` | normalises over the batch (and spatial) axes with the current batch's statistics; channel axis 1 | | `dropout_layer(p=0.1)` | active only with `train=true`; keeps the expected value (inverted dropout) | | `scale(d, shift=true)` | learnable per-feature `x · s + t` | ### Activations `activation(name)` with `name` one of `relu`, `gelu` (tanh approximation), `silu` / `swish`, `mish`, `elu`, `leaky_relu` (slope 0.01), `tanh`, `sigmoid`, `softplus`, `softmax` (last axis), `linear` / `identity` — or `activation(any_function)`, e.g. `activation(fn(x) => x * sigmoid(1.702 * x))`. Inside `sequential([...])` a name can be written directly: `sequential([linear(2, 8), "relu", linear(8, 1)])`. ### Small helpers | Constructor | Meaning | |---|---| | `identity()` | returns its input | | `select(i)` | element `i` of a list input (e.g. one branch of `parallel(..., combine="list")`) | | `lambda_layer(fn, name="lambda")` | any function `fn(x)` as a layer without parameters | | `add()`, `multiply()`, `concatenate()` | combine the list of inputs of a `dag` node | ## 4. Composition ### `sequential(layers)` Applies the layers in order. Parameters: a list. ```apsi let net = sequential([conv2d_layer(1, 16), "relu", max_pool(2), flatten_layer(), linear(16 * 14 * 14, 10)]) ``` ### `residual(inner, pre_norm=none)` `x + inner(x)`; with `pre_norm=layer_norm_layer(d)` it is `x + inner(norm(x))`. Parameters `record(inner, norm)`. ### `parallel(branches, combine="concat")` Runs every branch on the same input and combines the results: `"concat"` (last axis), `"add"`, `"mul"`, `"mean"` or `"list"` (keep them separate). Parameters: a list, one entry per branch. ### `dag(nodes, inputs=["x"], output=…)` — any directed acyclic graph Each node is `[name, layer, input]` or `[name, layer, [input1, input2, …]]`; a node with several inputs receives them as a list. Nodes may only use inputs or nodes defined before them, and every name is unique (both are checked when the model is created, with a clear message). `output` is a name or a list of names — the model then returns a list. ```apsi # two inputs, a shared trunk, two heads let net = dag([ ["img", cnn(1, 64, image=28), "image"], ["tab", mlp([12, 64, 64]), "table"], ["both", concatenate(), ["img", "tab"]], ["trunk", mlp([128, 128, 64]), "both"], ["cls", linear(64, 10), "trunk"], ["reg", linear(64, 1), "trunk"], ], inputs=["image", "table"], output=["cls", "reg"]) let p = net.init(seed=0) let outs = net(p, [images, tables]) # [class logits, regression output] ``` Weight sharing: `siamese(encoder, combine, head)` (section 6) applies one encoder — one parameter set — to two inputs `[a, b]`; use it as a node of a larger `dag` when you need shared weights inside your own graph. ## 5. Graph neural networks ### The graph ```apsi let g = graph(edges=[[0, 1], [1, 2], [2, 0]], n=3, undirected=true) # or graph(src=…, dst=…, n=…) ``` | Option | Meaning | |---|---| | `edges` / `src`, `dst` | directed edges `from → to` | | `n` | number of nodes (default: largest id + 1) | | `undirected=true` | adds every edge in both directions | | `self_loops=true` | adds `i → i` for every node (the default; GCN/GAT expect it) | | `batch=[…]` | the graph id of every node, for graph-level tasks with many small graphs in one batch | A graph has `.n`, `.edges`, `.src`, `.dst`, `.in_degree`, `.out_degree` and `.adjacency()`. ### Graph layers (node features `[N, D]`; apply with `model(p, x, graph=g)`) | Constructor | Update | |---|---| | `gcn(n_in, n_out, bias=true)` | `h_i = Σ_j x_j W / √(deg_i · deg_j)` (Kipf & Welling) | | `sage(n_in, n_out, aggregate="mean")` | `h_i = x_i W_self + agg_{j→i} x_j W_neigh` with `mean`, `sum` or `max` | | `gat(n_in, n_out, heads=4, concat=true, slope=0.2)` | graph attention; heads concatenated (`[N, heads·n_out]`) or averaged | | `gin(n_in, n_out, hidden=n_out)` | `MLP((1 + ε) x_i + Σ_j x_j)` with a learnable ε | | `graph_pool(how="mean")` | `[N, D]` → `[graphs, D]` using `batch` (`mean`, `sum`, `max`) | ### Differentiable graph operations `segment_sum(data, ids, n)`, `segment_mean`, `segment_max`, `segment_softmax(scores, ids, n)` (softmax within each segment, e.g. over the incoming edges of every node), `segment_count(ids, n)`, `gather(x, ids)`. With these you can write any message-passing layer yourself (see section 9). ### Node classification with a training mask ```apsi let r = fit(gat_net(8, 16, classes, heads=4), …, features, labels, loss="cross_entropy", graph=g, mask=train_mask, val=…, epochs=200) ``` The whole graph is used in every step; only nodes where `mask` is true count in the loss. ## 6. The model zoo | Model | Input → output | |---|---| | `mlp(sizes, act="relu", dropout=0.0, norm=false, out_act=none)` | e.g. `mlp([2, 64, 64, 3])` | | `resnet_mlp(d_in, d_out, width=64, blocks=3, act="gelu")` | deep MLP with pre-norm residual blocks — trains stably at any depth | | `cnn(in_channels, classes, image=28, channels=[16, 32], dense_width=64)` | `[B, C, image, image]` → `[B, classes]` | | `lstm_classifier(n_in, hidden, classes, layers=1)` | `[B, T, n_in]` → `[B, classes]` | | `transformer_lm(vocab, d=64, heads=4, layers=2, context=128, dropout=0.0)` | GPT-style: token ids `[B, T]` → next-token logits `[B, T, vocab]` | | `transformer_classifier(vocab, classes, d=64, heads=4, layers=2, context=128)` | token ids `[B, T]` → `[B, classes]` | | `autoencoder(d_in, latent, hidden=[64], act="relu")` | returns `[reconstruction, code]` | | `vae(d_in, latent, hidden=[64], act="relu")` | returns `[reconstruction, mu, logvar]`; train with `loss="vae"` (or `vae_loss(out, x, beta)`) | | `gcn_net(n_in, hidden, classes, layers=2, dropout=0.5)` | node classification | | `gat_net(n_in, hidden, classes, heads=4, dropout=0.3)` | node classification with attention | | `gin_net(n_in, hidden, classes, layers=3, pool="sum")` | graph classification (uses the graph's `batch`) | | `siamese(encoder, combine="concat", head=none)` | one encoder with shared weights applied to inputs `[a, b]` | Every zoo model is an ordinary composition of the layers above, so `summary(model)` shows exactly what is inside and you can copy and change it. ## 7. Training ### `fit` ```apsi let r = fit(model, params, X, y, loss="cross_entropy", opt=adam(lr=1e-3), epochs=20, batch_size=64) print(r.history[-1]) # loss of the last epoch let trained = r.params ``` | Argument | Default | Meaning | |---|---|---| | `model`, `params` | | the model and its starting parameters | | `X`, `y` | | inputs and targets (a list of inputs for models with several inputs). Without `y` the target is `X` (autoencoders, VAEs). | | `loss` | `"mse"` | `"mse"`, `"mae"`, `"cross_entropy"` (integer labels), `"bce"` (logits), `"vae"`, `"huber"` (torch backend), or a function `loss(prediction, target)` | | `opt` | `adam(1e-3)` | any optimizer of `use nn`: `sgd(lr, momentum)`, `adam(lr, b1, b2, eps)`, `adamw(lr, …, weight_decay)` | | `epochs` | `10` | passes over the data | | `batch_size` | all | mini-batch size; batches are reshuffled every epoch (reproducibly from `seed`) | | `seed` | `0` | shuffling, dropout and sampling | | `clip` | none | clip the global gradient norm to this value | | `schedule` | none | a function `step → learning rate`, e.g. `fn(s) => cosine_lr(s, 1000, base=3e-4, warmup=100)` | | `graph`, `mask` | none | graph tasks (section 5) | | `val` | none | `[X_val, y_val]`; the validation loss is recorded every epoch | | `verbose` | `true` | print the loss about ten times during training | | `workers` | `1` | data-parallel training on several processes (Linux, macOS): every batch is split into shards, their gradients are averaged by shard size — the same step as one process — then one optimizer step is taken. `"auto"` uses every core. It pays off when the model's matrix products are too small to keep all cores busy on their own | | `backend` | `"numpy"` | `"numpy"`, `"torch"` or `"auto"` — see [`BACKENDS.md`](/docs/2.4.4/backends) | | `device`, `precision`, `compile` | `"auto"`, `"fp32"`, `false` | torch backend only | `fit` returns `record(params, history, val_history, device)` — `device` is `"numpy"` or the torch device used. ### Your own training loop `fit` is a convenience. Any loop works, because the model is a function of its parameters: ```apsi let opt = adamw(lr=3e-4, weight_decay=0.01) var p = model.init(seed=0) for step in 0..1000: let xb, yb = next_batch() let l, g = value_and_grad(fn(q) => cross_entropy(model(q, xb, train=true), yb))(p) p = opt.step(p, clip_grad_norm(g, 1.0)) ``` ### `predict` `predict(model, params, X, graph=…, backend="numpy")` applies the model with `train=false`. ## 8. Inspection, saving and loading - `summary(model, params)` prints a table of every layer and its parameter count, and returns it as a string - `count_params(params)` — the number of trainable numbers - `save_params("model.npz", params)` / `load_params("model.npz", model.init())` — portable NumPy archives - `quantize_model(params, int8)` and `qlinear` shrink a trained model (see [`AI.md`](/docs/2.4.4/ai#3-use-less-ram--vram)) - on the torch backend, `torch_module(model, params)` gives a real `torch.nn.Module` (`state_dict`, ONNX export, …) ## 9. Writing your own layer Any function `fn(params, x)` already is a model you can differentiate. To make it a reusable layer with its own parameters, put it in a `dag`/`sequential` through `lambda_layer` (no parameters), or keep the parameters next to the model in a record: ```apsi use layers use nn # a gated message-passing layer written with the segment operations fn gated_mp(p, x, g): let msg = gather(x, g.src) @ p.W let gate = sigmoid(gather(x, g.dst) @ p.G) return x + segment_mean(msg * gate, g.dst, g.n) let p = record(W=randn(8, 8, seed=0) * 0.1, G=randn(8, 8, seed=1) * 0.1) let l, grads = value_and_grad(fn(q) => mean(gated_mp(q, features, g) ** 2))(p) ``` Mix both styles freely: `record(backbone=net.init(seed=0), head=dense(64, 1))` is a valid parameter tree, and `fn model(p, x) = net(p.backbone, x) @ p.head.W + p.head.b` is a valid model. ## 10. Recipes **Image classifier** ```apsi let net = cnn(1, 10, image=28, channels=[32, 64], dense_width=128) let r = fit(net, net.init(seed=0), images, labels, loss="cross_entropy", opt=adam(lr=1e-3), epochs=5, batch_size=128) ``` **Character-level GPT** ```apsi let lm = transformer_lm(vocab_size, d=128, heads=4, layers=4, context=128, dropout=0.1) let r = fit(lm, lm.init(seed=0), tokens[:, 0:-1], tokens[:, 1:], loss="cross_entropy", epochs=10, batch_size=32, schedule=fn(s) => cosine_lr(s, 5000, base=3e-4, warmup=200), clip=1.0) ``` **Sequence regression with an LSTM** ```apsi let net = sequential([lstm_layer(3, 64), lstm_layer(64, 64, return_sequences=false), linear(64, 1)]) ``` **Wide & deep** ```apsi let net = dag([["wide", linear(20, 1), "x"], ["deep", mlp([20, 128, 64, 1]), "x"], ["out", add(), ["wide", "deep"]]]) ``` **Variational autoencoder** ```apsi let v = vae(784, 16, hidden=[256]) let r = fit(v, v.init(seed=0), images_flat, loss="vae", epochs=20, batch_size=128) let recon = predict(v, r.params, images_flat)[0] ``` **Graph classification** — many small graphs in one batch: ```apsi let g = graph(src=all_src, dst=all_dst, n=total_nodes, batch=graph_id_of_node) let net = gin_net(n_features, 64, classes, layers=3) let r = fit(net, net.init(seed=0), node_features, graph_labels, loss="cross_entropy", graph=g, epochs=100) ``` ## 11. Errors and how to fix them | Message | Cause and fix | |---|---| | `sequential: 3 layers but 2 parameter entries` | the parameters belong to another model — create them with `model.init()` | | `dag(): node 'h2' uses 'h3', which is not defined before it` | order the nodes so every input comes first | | `graph layers need the graph: model(params, X, graph=g)` | pass `graph=g` to the model, `fit` and `predict` | | `multi_head_attention: d=30 must be divisible by heads=4` | choose `d` as a multiple of `heads` | | `unknown activation 'reLU'` | names are lower case; the message lists every valid name | | `fit(): unknown loss` | use one of the listed names or pass a function | | `fit(backend="torch") needs permission 'python'` | the torch backend runs PyTorch: `apsi run --allow-python app.apsi` | | `backend="torch" needs PyTorch` | `apsi pkg install torch-cuda` (NVIDIA) or `apsi pkg install torch` (CPU) — see [`BACKENDS.md`](/docs/2.4.4/backends#1-setup) | # Graphs that reach far Graph neural networks pass messages along edges: one layer moves information one hop. Two problems follow, and both are measurable: * **Values do not travel far.** In a post-norm network that also counts each node as its own neighbour, every layer dilutes what arrived from the neighbours; in practice values make it about three hops, however many layers there are. * **Everything is squeezed into one vector.** A node that has heard from eight leaves holds one summary of them and cannot say which leaf carried which value. A mean over the whole graph has the same problem. `use layers` has the tools large graph models use against both: | Tool | What it does | Reach | |---|---|---| | `graph_transformer(d, heads, relations)` | pre-norm residual block: attention over the neighbours only, separate key/value weights per relation, a learned bias per relation and per hop distance, then an MLP | one hop per layer, kept over many layers | | `global_attention(d, heads, max_keys=4096)` | every node attends to every node of its own graph by content; graphs of a batch never see each other; above `max_keys` nodes the keys are an evenly strided subset | **any distance in one layer** | | `k_hop(g, k)` | adds a shortcut edge to everything within `k` hops (`edge.hop` = distance) | k hops per layer | | `mix_hop(n_in, n_out, hops=[0, 1, 2])` | several distances in parallel in one layer: `[x, Âx, ²x, …]`, each with its own weights | parallel hops | | `graph_memory(d, k)` | the `k` most salient node states of every graph, weighted by a learned score: `[graphs, k, d]` for a decoder to attend into | the whole graph, node by node | | `graph_transformer_net(n_in, d, out, layers, heads, relations, global_every, task)` | a complete model: input projection, local blocks with a global block every `global_every` layers, final norm, node or graph output | all of the above | | `hop_distances(g, node)`, `eccentricity(g)` | breadth-first hop counts; the longest shortest path | measuring how far information must go | ## Relations and both directions ```apsi let g = graph(edges=edges, n=n, types=kinds, reverse=true, batch=graph_of_node) ``` `types` gives every edge a relation (`0`, `1`, …), for example syntax, data flow and calls in a program graph. `reverse=true` adds every edge backwards as a relation of its own, so information flows both ways with separate weights (from a use back to its definition, from a callee back to its caller). `graph_transformer(…, relations= g.relations)` learns one key and value projection per relation. ## A task that needs reach Node 0 of each chain must report a class written at the other end, five hops away (the full program is `examples/42_long_range_graphs.apsi`): ```apsi let model = graph_transformer_net(4, 16, 2, layers=1, heads=2, relations=2, global_every=1) let r = fit(model, model.init(seed=0), X, y, loss="cross_entropy", opt=adam(lr=0.01), epochs=150, graph=g, mask=mask, verbose=false) ``` Measured by the test suite (`tests/test_graphs.py`): | Model | Accuracy at node 0 | |---|---:| | 1 `graph_transformer` + 1 `global_attention` | 100 % (one global step reaches the far end) | | 6 `graph_transformer` blocks, no global attention | 100 % (values survive six layers) | | 2 `graph_transformer` blocks | at chance (the far end is out of reach) | ## Cost * `graph_transformer`: proportional to the number of edges, like `gat`. * `global_attention`: nodes × keys per graph (keys = nodes up to `max_keys`, then `max_keys`). For large graphs put a global block after every second local block, as `global_every=2` does. * `k_hop`: the number of edges grows with the k-hop neighbourhood; use it for sparse graphs and small `k`. * All of these train with `fit(…)` and Apsi's own autodiff; `fit(…, workers=N)` spreads mini-batches over cores. # From Python to Apsi (`apsi convert`) `apsi convert` translates Python files — or whole projects — into Apsi **with Python's exact semantics**: the converted program prints the same text, raises the same errors, trains to the same losses and writes the same files as the Python program, byte for byte. Large real programs convert in seconds and run immediately. ```bash apsi convert script.py # writes script.apsi (and .apsi files for the modules it imports), checks them apsi convert script.py --run # convert, check and run apsi convert src/ -o apsi_src/ # a whole project, keeping the folder layout apsi convert script.py --stdout # print the Apsi code apsi convert script.py --idiomatic # map numpy/math/torch onto Apsi's own built-ins (small numeric scripts) ``` In VS Code, Cursor and the other VS Code-family editors: right-click a `.py` file (or a folder) → **Apsi: Convert Python File to Apsi**. Typical output: ```text ✓ build_graph_cache.py → build_graph_cache.apsi (0 notes) ✓ GLA.py → GLA.apsi (0 notes) run with --allow-python=all (it uses subprocess) ✓ gla_brain_dual.py → gla_brain_dual.apsi (0 notes) converted 5 files, 0 notes ``` Modules that sit next to the program (`import GLA`, `from gla_brain_dual import X`, packages with `__init__.py`, `from . import x`) are converted with it and imported as Apsi files; everything else is loaded from Python. ## How exact is exact? The converted file starts with `use pycompat`. That line gives the file Python's object model and built-ins while the text stays Apsi (`fn`, `let`/`var`, `none`/`true`/`false`, `import "x.apsi"`). Concretely: | Python | Apsi (with `use pycompat`) | behaves | |---|---|---| | `class Net(nn.Module):` with `__init__`, `super().__init__()`, `forward`, `@property`, `@staticmethod`, `@classmethod`, `__slots__`, dunder methods, metaclasses, class decorators, `__init_subclass__` | `class Net(nn.Module):` with `fn __init__(self, …):` … | a real class: inherits from any library class, attribute hooks (`__setattr__`, `__getattr__`) run, pickling works | | `@dataclass(frozen=True, order=True)` with `x: int = 0`, `field(default_factory=list)` | the same decorator and `x: int = 0` fields | Python's dataclass (`asdict`, `fields`, `replace`, frozen errors …) | | `with open(p) as f, lock:` / `async with` | `with open(p) as f, lock:` | `__enter__`/`__exit__`, exceptions suppressed when `__exit__` returns true | | `try / except (A, B) as e / else / finally`, `raise X from Y`, own exception classes | the same | the real exception object is bound (`e.args`, `e.__cause__`, `isinstance`) | | generators, `x = yield v`, `yield from`, generator expressions | the same | lazy, `send()` / `throw()` / return values | | `global`, `nonlocal`, closures, default values evaluated once, `*args` (a tuple), keyword-only and positional-only parameters | the same | Python's scoping: one namespace per function, loop variables live after the loop | | `a, *rest = xs`, `for i, (a, b) in …`, `a = b = 0`, `x += [1]` (in place) | the same | | | `{1, 2}`, set/dict comprehensions, `{**a, **b}`, `[*xs, y]` | the same | | | `is`, `is not`, `a < b < c` (b evaluated once), `| & ^ << >> ~`, `:=` | the same | | | `for … else`, `while … else`, `del`, `assert`, `pass` | the same | | | f-strings with `!r`, `{x=}`, nested specs `{v:{w}.{p}f}` | f-strings, or `format()` calls when an expression contains quotes | Python's `format()` | | `print`, `str`, `repr`, `len`, `range`, `map`, `zip`, `sorted`, `int`, `isinstance`, `super`, … | Python's own built-ins | `print([None, True])` prints `[None, True]` | | `import x.y as z`, `from x import y` | `let z = pyimport("x.y")`, `let y = pyfrom("x", "y")` | submodules found as Python does | | `if __name__ == "__main__":` | the same | `__name__` is `"__main__"` only for the program you run; imported files get their module name | | docstrings | kept (they are docstrings again: `help()` and `__doc__` work) | | | comments, blank lines, statement order | kept | | | `match` / `case` with literal, capture, sequence (`[x, *rest]`), mapping (`{"k": v, **rest}`), class (`Point(x=0, y=y)`), or (`1 \| 2`), `as` and guard patterns | the same | Python's matching rules, including `__match_args__` | | `except*` and exception groups | the same | | | `except KINDS as e:` where `KINDS` is a variable or call | an `isinstance` test on the caught error | the same handler runs | | `async def`, `await`, `async for`, `async with`, async comprehensions | the same | runs on asyncio | | `[b.v for b.v in xs]`, `for st.line, x in …` (attribute and index targets) | the same | | | `type Pair = tuple[int, int]`, `def first[T](xs: list[T]) -> T`, `class Box[T]:` | `TypeAliasType`, `TypeVar` and `Generic[T]` objects | | | `self.__secret` inside `class Vault` | the same | stored as `_Vault__secret` (Python's name mangling) | | `from math import *`, `from helper import *` | the module's public names (its `__all__` when it has one) | | | `a[1:2, ...]`, `a[*idx]`, `a[1,]` | the same | | | reading a name before it is bound | the same | `UnboundLocalError` / `NameError`, as in Python | | names that are Apsi keywords (`let`, `var`, `fn`, `struct`, `use`, `none`, `true`, `false`, `match`, `type` …) | written in backticks: `` `let` ``, `` `fn` `` | the same name — `getattr`, keyword arguments and pickling see `let`, not a renamed copy | Converted programs run at the speed of the Python program — attribute access, calls and loops compile to the same Python bytecode — and keep every Apsi feature: `use rt`, `use hp`, `@native` functions, units, the checker. Measured on 2 cores (`benchmarks/python/cmp.sh`, best of five, interleaved): | program | CPython 3.13 | converted, `apsi run` | |---|---|---| | objects: trees, dataclasses, dicts, `Counter` | 0.295 s | **0.271 s** (8 % faster) | | strings: regex, `split`, JSON, sets | 0.271 s | 0.273 s | | n-body, 200,000 steps (floats, lists) | 0.928 s | 0.941 s | Programs that create many objects are faster because Apsi freezes everything loaded before the program starts (the cycle collector never scans it again) and collects the youngest generation every 50,000 allocations instead of 2,000; reference counting is untouched, so objects are still freed the moment they are no longer used (`APSI_GC=default` restores Python's settings). Pure arithmetic in Python loops runs on the same bytecode interpreter as CPython, so it runs at the same speed; to go far beyond it, put `@native` on the hot function (machine code through LLVM, see [native code](/docs/2.4.4/native)). Conversion itself: the 23,769-line program and three of its modules (29,022 lines) convert and check in 4.5 s. ## Permissions — what a converted program may touch Apsi's sandbox stays on. Without any flag a converted program can use: * every **pure** standard module (`collections`, `itertools`, `functools`, `re`, `json`, `dataclasses`, `enum`, `typing`, `math`, `random`, `decimal`, `time`, `argparse`, `threading`, `queue`, `datetime`, `hashlib` …) * **`sys`, `os`, `os.path`, `pathlib` and `numpy` as permission-checked stand-ins**: the same names and behaviour, but file access asks `--allow-read`/`--allow-write`, environment variables ask `--allow-env`, and the parts that would escape the sandbox (`os.system`, `subprocess`, `sys.modules`, `numpy.ctypeslib`, `np.load(allow_pickle=True)`, `array.tofile`) are not there * `open()` checked against `--allow-read` / `--allow-write` Other libraries need `--allow-python` (for example `torch`, `scipy`, `pandas`), and libraries that reach the operating system or the network (`subprocess`, `socket`, `http`, `ctypes`, `multiprocessing`, `shutil` …) need `--allow-python=all`; with `=all` every module is the real one and nothing is filtered. `apsi convert` prints the flag a file needs, and `apsi run` says so when one is missing: ```bash apsi run train.apsi --allow-python # torch, numpy, scipy … apsi run GLA.apsi --allow-python=all -- train --epochs 3 # arguments after -- go to the program (sys.argv) ``` Introspection that leads to interpreter internals (`__globals__`, `__subclasses__`, `__builtins__`, frame objects, `object.__getattribute__`, `__dict__` of library classes, `operator.attrgetter("__…")`, `eval`/`exec`/`__import__`) is refused unless the program was started with `--allow-python=all`. ## Checked with real programs * **The Python standard library itself.** `tests/test_stdlib_convert.py` copies 34 pure-Python standard modules next to a driver program — `_pydecimal` (6,357 lines of arbitrary-precision decimal arithmetic), `_pydatetime`, `argparse`, `configparser`, `tomllib`, `json` (a package with relative imports), `pickle`, `dataclasses`, `enum`, `random`, `statistics`, `fractions`, `difflib`, `ipaddress`, `textwrap`, `pprint`, `calendar`, `shlex`, `graphlib`, `heapq`, `bisect`, `html.parser`, `urllib.parse`, `base64`, `fnmatch`, `string`, `copy`, `contextlib`, `operator`, `reprlib`, `numbers`, `_py_abc`, `colorsys`, `keyword` — converts all of them with the program, runs it, and requires **byte-identical output** to CPython running the original files. About 38,000 lines of metaclasses, descriptors, generators, `__slots__`, name mangling, `__class_getitem__`, exception chaining and 50-digit arithmetic. * `tests/pycompat/` — programs with classes, inheritance from library classes, dataclasses, enums, abstract base classes, context managers, generators with `send`, closures, threads, `collections`, `heapq`, `functools`, `re`, `json`, numpy and every operator. `tests/test_python_compat.py` converts each one and requires **byte-identical output**. * A 23,769-line AI training program with its four companion modules (graph building for 30 languages, web search, safety filter, a dual-brain model) converts in about 3 seconds and checks cleanly; its command-line tools, its multi-process graph-cache builder (results pickled across worker processes, resumable output) and its safety filter produce byte-identical files and output to Python. * A PyTorch-style training program (an `nn.Module` hierarchy that registers parameters through `__setattr__`, autograd, Adam with weight decay, cosine schedule, early stopping, checkpoint) trains to bit-identical losses. ## Coverage Every statement and expression of Python 3.13 converts: there is no "kept as a comment" list any more. Two things differ by design and are not part of a program's behaviour: * **Tracebacks** of an uncaught error name the `.apsi` file and line (`train.apsi:212:1: TypeError: …`) instead of the `.py` file; the error type and message are Python's. Programs that catch errors see the real exception objects. * **The sandbox.** Without `--allow-python=all`, introspection that leads to interpreter internals is refused (see above). With it, `eval`, `exec`, `globals()`, frames and every module behave exactly as in Python. A converted module is reused while it is newer than its `.py` file and was written by the same Apsi version; a new Apsi version converts it again. ## `--idiomatic`: PyTorch programs → native Apsi (no Python, no PyTorch) A PyTorch training script becomes an Apsi program that runs **without Python and without PyTorch**. The converter targets `use torch`, Apsi's own implementation of the PyTorch API: the same layer names, constructor arguments, parameter names (`fc1.weight` stored as `[out, in]`), buffers, losses, optimizers and learning-rate schedulers, implemented natively on Apsi's automatic differentiation. Nothing is approximated and nothing is left for you to finish by hand. ```python import torch import torch.nn as nn import torch.nn.functional as F class MLP(nn.Module): def __init__(self, d_in, hidden, d_out): super().__init__() self.fc1 = nn.Linear(d_in, hidden) self.bn = nn.BatchNorm1d(hidden) self.out = nn.Linear(hidden, d_out) self.drop = nn.Dropout(0.1) def forward(self, x): return self.out(self.drop(F.relu(self.bn(self.fc1(x))))) X = torch.randn(256, 4) y = (X[:, 0] > 0).long() model = MLP(4, 32, 2) opt = torch.optim.AdamW(model.parameters(), lr=1e-2) sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=200) loss_fn = nn.CrossEntropyLoss(label_smoothing=0.1) for epoch in range(200): opt.zero_grad() logits = model(X) loss = loss_fn(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() sched.step() model.eval() print(loss.item()) torch.save(model.state_dict(), "mlp.safetensors") ``` `apsi convert mlp.py --idiomatic` writes: ```apsi # Converted from mlp.py by `apsi convert`. use models as models use torch as torch var _aux3 = none var _grads2 = none var _loss_of1 = none var loss = none fn MLP(d_in, hidden, d_out): let fc1 = torch.Linear(d_in, hidden) let bn = torch.BatchNorm1d(hidden) let out = torch.Linear(hidden, d_out) let drop = torch.Dropout(0.1) fn init_(seed = 0) = record(fc1=fc1.init(seed=seed * 1009 + 1), bn=bn.init(seed=seed * 1009 + 2), out=out.init(seed=seed * 1009 + 3), drop=drop.init(seed=seed * 1009 + 4)) fn forward_(p, x, train = false) = out(p.out, drop(p.drop, torch.relu(bn(p.bn, fc1(p.fc1, x, train=train), train=train)), train=train), train=train) return torch.Module(init_, forward_, "MLP", record(fc1=fc1, bn=bn, out=out, drop=drop)) let X = randn(256, 4) let y = tensor(X[:, 0] > 0, dtype=i64) let model = MLP(4, 32, 2) var model_params = model.init(seed=0) let opt = torch.AdamW(lr=0.01) let sched = torch.CosineAnnealingLR(opt, T_max=200) let loss_fn = torch.CrossEntropyLoss(label_smoothing=0.1) for epoch in 0..200: fn _loss_of1(_p): let logits = model(_p, X) let loss = loss_fn(logits, y) return loss loss, _aux3, _grads2 = torch.value_and_grad_aux(_loss_of1)(model_params) _grads2 = torch.clip_grad_norm(_grads2, 1.0) model_params = opt.step(model_params, _grads2) sched.step() model.eval() print(loss) models.save_safetensors("mlp.safetensors", torch.state_dict(model, model_params)) ``` The saved file is a standard safetensors file with PyTorch's names (`bn.running_mean`, `fc1.weight`, …): `model.load_state_dict(load_file("mlp.safetensors"), strict=True)` in PyTorch accepts it unchanged, and the other direction works the same way. ### What is converted exactly | PyTorch | Apsi (`use torch`) | |---|---| | `class Net(nn.Module)` with `__init__`, `forward` and helper methods; sub-modules of your own classes; `nn.Sequential`, `nn.ModuleList` | `fn Net(...)` returning `torch.Module(init, forward)`; parameters are an explicit tree (`p.fc1.weight`) | | `nn.Linear`, `Bilinear`, `Conv1d`, `Conv2d` (stride, padding including `"same"`, dilation, groups), `MaxPool1d/2d`, `AvgPool1d/2d` (`ceil_mode`, `count_include_pad`), `AdaptiveAvgPool1d/2d`, `AdaptiveMaxPool1d/2d`, `Embedding` (`padding_idx`), `Dropout`, `Flatten`, `Unflatten`, `Identity` | the same layers with the same arguments | | `BatchNorm1d/2d` (running statistics, `momentum`, `momentum=None`, `track_running_stats`), `LayerNorm`, `RMSNorm`, `GroupNorm` | identical training and evaluation behaviour, buffers included in `state_dict` | | `LSTM`, `GRU`, `RNN` (`num_layers`, `bidirectional`, `batch_first`, `dropout`, initial states) | returns the same `(output, (h_n, c_n))` tuples | | `MultiheadAttention` (`kdim`/`vdim`, `key_padding_mask`, boolean or float `attn_mask`, `need_weights`, `average_attn_weights`, `is_causal`), `TransformerEncoder(Layer)`, `TransformerDecoder(Layer)`, `Transformer` (`norm_first`, activation) | same results, including attention weights | | all activation modules and `F.` functions (`gelu` exact or `tanh`, `silu`, `mish`, `softplus`, `glu`, …), `F.pad`, `F.normalize`, `F.one_hot`, `F.scaled_dot_product_attention`, `F.cosine_similarity` | same names under `torch.` | | `CrossEntropyLoss` (`weight`, `ignore_index`, `label_smoothing`, probability targets, N-d inputs), `NLLLoss`, `MSELoss`, `L1Loss`, `SmoothL1Loss`, `HuberLoss`, `BCELoss`, `BCEWithLogitsLoss` (`pos_weight`), `KLDivLoss`; every `reduction` | exact formulas | | `torch.optim.SGD` (momentum, Nesterov), `Adam` (`amsgrad`), `AdamW`, `Adamax`, `RMSprop`, `Adagrad`, `NAdam`, `RAdam` | identical update rules | | `lr_scheduler.StepLR`, `MultiStepLR`, `ExponentialLR`, `CosineAnnealingLR`, `CosineAnnealingWarmRestarts`, `LinearLR`, `ConstantLR`, `LambdaLR`, `MultiplicativeLR`, `PolynomialLR`, `OneCycleLR` (with momentum cycling), `CyclicLR`, `ReduceLROnPlateau`, `SequentialLR`, `ChainedScheduler` | identical learning-rate sequences; `sched.step()` is kept | | `opt.zero_grad(); loss.backward(); clip_grad_norm_ / clip_grad_value_; opt.step()` | one `value_and_grad_aux` call, clipping, `opt.step(params, grads)`; values computed in the forward pass (logits, accuracy) remain available | | `model.train()` / `model.eval()`, `torch.no_grad()`, `.to(device)`, `.item()`, `.detach()` | kept or removed as appropriate | | `DataLoader(TensorDataset(...))`, `DataLoader` over your own `Dataset` class (`__len__`, `__getitem__`), `batch_size`, `shuffle`, `drop_last`, `collate_fn` | `torch.DataLoader` with the same behaviour | | `torch.utils.checkpoint.checkpoint` | `torch.checkpoint` — recomputes activations in the backward pass to save memory | | `model.state_dict()`, `load_state_dict(...)`, `torch.save` / `torch.load`, `safetensors.torch.save_file` / `load_file` | `torch.state_dict`, `torch.load_state_dict`; files ending in `.safetensors` stay safetensors, others become `.apsim` | | tensor methods and functions: `view`, `reshape`, `flatten`, `contiguous`, `size`, `float`, `long`, `sum(dim=)`, `mean`, `argmax`, `max(dim=)`, `transpose`, `permute`, `unsqueeze`, `squeeze`, `clamp`, `exp`, `matmul`, `torch.cat`, `torch.stack` | the matching Apsi functions | **Verified exactness.** The test suite (`tests/test_convert_torch.py`, `tests/test_torch_compat.py`) runs every layer, loss, optimizer and scheduler above next to real PyTorch with identical weights and compares outputs, gradients and learning-rate sequences. A complete training run — convolution, BatchNorm, LSTM, multi-head attention, AdamW, cosine annealing, gradient clipping and label smoothing — ends with the same weights as PyTorch to within 1e-9. Random initialisation uses Apsi's generator, so a model trained *from scratch* starts from different random weights than in PyTorch. To reproduce a PyTorch run exactly, load its initial `state_dict`. ### Memory - `torch.checkpoint_layer(layer, params, x)` or `torch.checkpoint(fn, x)` stores only the block inputs and recomputes the rest during the backward pass: about a third of the peak memory for about twice the compute. - Create parameters with `dtype=f32` (or `fit(…, precision="fp32")`); gradients and Adam state then stay float32. - `DataLoader` builds one batch at a time, so datasets larger than memory can be streamed from a `Dataset` class. After converting with `--idiomatic`, run `apsi check file.apsi` (done automatically) — and consider adding units to the physics parts: `let g = 9.81` → `let g = 9.81[m/s^2]` turns every later formula into a checked one. # Reinforcement learning `use rl` provides environments, advantage estimation, policy helpers, complete training algorithms (PPO, DQN, REINFORCE), the losses used to train language models from human preferences (RLHF, DPO, IPO, GRPO), and search algorithms (Monte Carlo tree search, beam search, best-of-n). Everything runs on the CPU without external libraries and works with any model written with `use torch` or `use layers`. ## A complete example: PPO on CartPole ```apsi use torch use rl let l1 = torch.Linear(4, 64) let l2 = torch.Linear(64, 64) let pi = torch.Linear(64, 2) let vf = torch.Linear(64, 1) let params = record(l1=l1.init(1), l2=l2.init(2), pi=pi.init(3), vf=vf.init(4)) # the model returns [action logits, state value] fn policy(p, obs): let h = tanh(l2(p.l2, tanh(l1(p.l1, obs)))) return [pi(p.pi, h), vf(p.vf, h)] let result = rl.ppo(rl.cartpole(8, seed=0), policy, params, total_steps=200000, epochs=10, lr=1e-3) let score = rl.evaluate(rl.cartpole(1, seed=5), fn(obs) => argmax(policy(result.params, obs)[0], axis=1), episodes=5) print(f"mean return: {score.mean}") # 500 is the maximum ``` With these settings PPO reaches the maximum return of 500 on CartPole-v1 in about 200 000 environment steps. ## Environments | Function | Task | |---|---| | `cartpole(n=1, seed, max_steps=500)` | Balance a pole on a cart. Dynamics identical to Gymnasium’s CartPole-v1. | | `gridworld(size=5, n=1, seed)` | Walk to the goal on a grid; each step costs a little reward. | | `bandit(arms=10, n=1, seed)` | Multi-armed Gaussian bandit. | All environments are **vectorised**: `n` copies run side by side. `reset(seed)` returns observations of shape `[n, …]`; `step(actions)` returns `record(obs, reward, done, truncated, final_obs)`. Finished copies reset themselves, and `final_obs` holds the last observation before the reset. Completed episode returns are collected in `env.episode_returns`. Your own environment only needs the same members: `n`, `reset(seed)`, `step(actions)` and `episode_returns`. ## Algorithms ### `ppo(env, model, params, …)` Proximal Policy Optimization with the clipped surrogate objective, generalised advantage estimation, a value loss, an optional entropy bonus, advantage normalisation, global gradient-norm clipping and linear learning-rate annealing. | Argument | Default | Meaning | |---|---|---| | `total_steps` | 100 000 | environment steps across all copies | | `rollout` | 128 | steps per copy between updates | | `epochs` | 4 | passes over each rollout | | `minibatch` | 256 | samples per gradient step | | `lr` | 3e-4 | Adam learning rate | | `gamma`, `lam` | 0.99, 0.95 | discount and GAE parameter | | `clip` | 0.2 | ratio clipping range | | `vf_coef`, `ent_coef` | 0.5, 0.0 | weights of the value loss and entropy bonus | | `max_grad_norm` | 0.5 | gradient clipping | | `anneal_lr` | true | decay the learning rate linearly to 0 | | `opt` | Adam | any optimizer from `use torch` | `model(params, obs)` returns `[logits, value]`. The result is `record(params, episode_returns, mean_returns, updates)`. ### `dqn(env, model, params, …)` Double DQN with a replay buffer, a target network, ε-greedy exploration and the Huber loss. `model(params, obs)` returns one value per action. ### `reinforce(env, model, params, episodes, …)` Monte Carlo policy gradient with a running baseline. ### `evaluate(env, policy, episodes=10, seed=0)` Runs a deterministic policy (`policy(obs)` returns actions) and returns `record(mean, returns)`. ## Building blocks | Function | Purpose | |---|---| | `discounted_returns(rewards, dones, gamma, last_value)` | discounted returns that stop at episode ends | | `gae(rewards, values, dones, last_values, gamma, lam)` | generalised advantage estimation; returns `record(advantages, returns)` | | `categorical_sample(logits, seed)` · `categorical_log_prob(logits, actions)` · `categorical_entropy(logits)` | discrete policies | | `gaussian_log_prob(mean, log_std, actions)` · `gaussian_entropy(log_std)` | continuous policies | | `sample_token(logits, temperature, top_k, top_p, seed)` | sampling for language models | ## Learning from preferences (RLHF, DPO, GRPO) | Function | Purpose | |---|---| | `preference_loss(r_chosen, r_rejected, margin)` | Bradley–Terry loss for training a reward model | | `dpo_loss(policy_chosen, policy_rejected, ref_chosen, ref_rejected, beta, label_smoothing)` | Direct Preference Optimization; inputs are sequence log-probabilities; returns `record(loss, chosen_rewards, rejected_rewards, accuracy)` | | `ipo_loss(…, tau)` | Identity Preference Optimization | | `kl_penalized_rewards(rewards, logp, ref_logp, beta)` | per-token rewards with a KL penalty against a reference model (RLHF with PPO) | | `grpo_advantages(rewards, group_size)` | group-relative advantages: several answers per prompt, normalised within each group | | `ppo_clip_loss(logp, old_logp, advantages, clip, mask)` | the clipped surrogate for token sequences | | `sequence_log_prob(logits, tokens, mask)` | log-probability of each sequence, differentiable | A DPO step for a causal language model: ```apsi fn dpo_step(p): let pc = rl.sequence_log_prob(model(p, chosen), chosen_targets, chosen_mask) let pr = rl.sequence_log_prob(model(p, rejected), rejected_targets, rejected_mask) return rl.dpo_loss(pc, pr, ref_chosen, ref_rejected, beta=0.1).loss let loss, grads = value_and_grad(dpo_step)(params) params = opt.step(params, grads) ``` `ref_chosen` and `ref_rejected` are the same log-probabilities computed once with the frozen reference model. ## Search | Function | Purpose | |---|---| | `mcts(state, legal_actions, apply, is_terminal, reward, simulations, c, rollout_depth, seed, two_player)` | Monte Carlo tree search with UCT; returns `record(action, visits, values)` | | `beam_search(step, start, beam_width, max_len, eos)` | keeps the best partial sequences; `step(seq)` returns log-probabilities of the next token; returns a list of `record(tokens, score)`, best first | | `best_of_n(candidates, score)` | the highest-scoring candidate (rejection sampling); returns `record(best, index, scores)` | ## Memory and speed - Train in float32 with `fit(…, precision="fp32")` or by creating parameters with `dtype=f32`; gradients and optimizer state stay float32, which halves memory. - Wrap expensive blocks with `torch.checkpoint_layer(layer, params, x)` to recompute activations in the backward pass instead of storing them: roughly a third of the peak memory for about twice the compute. - Run many environment copies (`n=8` or more) so each model call processes a batch. # Model files Apsi saves and loads models in the formats the rest of the ecosystem uses, and in its own checkpoint format (`.apsim`) for everything those formats cannot hold: physical units, compressed tensors, the shape of the parameter tree, integrity checks and signatures. | Format | Save | Load | Inspect | Opens in | |---|---|---|---|---| | **Apsi checkpoint** `.apsim` | `save_model` | `load_model` | `model_info` | Apsi; any safetensors reader (PyTorch, JAX, Hugging Face) | | **GGUF** `.gguf` | `save_gguf` | `load_gguf` | `gguf_info` | llama.cpp, Ollama, LM Studio, any GGUF reader | | **safetensors** | `save_safetensors` | `load_safetensors` | `safetensors_info` | PyTorch, JAX, Hugging Face | | ONNX / PyTorch | — | `onnx_model`, `torch_load` | — | see [`AI.md`](/docs/2.4.4/ai) | All of them are in `use models`. Saving needs `--allow-write`, loading needs `--allow-read` (or the `fs` permission), exactly like other file functions, and `apsi audit` reports their use. ## 1. Apsi checkpoints (`.apsim`) ```apsi use models use precision let params = record(layers=[record(W=randn(4096, 4096), b=zeros(4096))], mass=tensor([1.0, 2.0]) * 1kg) # units are kept let small = quantize_model(params, int8) # compressed tensors are kept as they are save_model("run/model.apsim", small, metadata=record(arch="mlp", step=12000)) let back = load_model("run/model.apsim") # memory-mapped: opens instantly print(model_info("run/model.apsim")) ``` What the format guarantees: - **Standard container.** Each `.apsim` file is a valid safetensors file. Any safetensors reader gets the tensors (compressed tensors appear as their codes plus a `NAME__qscale` tensor). - **Exact round trip.** Records and lists come back as they were. Tensors keep their dtype and bits, `Quantity` values keep their unit, and QTensors (INT8, INT4, FP8, FP6, FP4, BF16) keep their codes, scales and format. - **Integrity.** Each file stores a SHA-256 of its tensor table (names, dtypes, shapes, offsets) and its data. `load_model` checks it by default and refuses a damaged or modified file. Use `verify=false` to skip the check on trusted storage when load time matters. - **Signatures.** `sign=true` signs with your per-user key (the same key that protects Apsi's caches), and `sign="shared secret"` signs with a key shared by a team. `load_model(path, key=true)` or `load_model(path, key="shared secret")` then refuses a file that is unsigned, was signed with another key, or was changed after signing. Signing uses HMAC-SHA256 over every shard digest and the Apsi metadata. Giving a key always turns verification on. - **Large models.** A checkpoint larger than `shard` (default `"4GB"`) is split into `NAME-00001-of-0000N.apsim` files plus `NAME.apsim.index.json` in the Hugging Face layout. Load the index file. Each shard digest is recorded in the index, so mixing shards from different checkpoints is detected. - **Safe parsing.** Loading never runs code from the file. Header size, every tensor offset and every size are checked against the file before anything is mapped. A malformed file gives a clear error, never a crash; this is covered by fuzz tests. - **Atomic writes.** Files are written to a temporary name and renamed, so an interrupted save never leaves a half-written checkpoint under the real name. ### Layout ``` u64 little-endian header length N N bytes JSON header (safetensors): tensor table + "__metadata__" data tensors, back to back, in table order ``` `__metadata__` keys written by Apsi (all values are strings, as safetensors requires): | Key | Content | |---|---| | `format` | `"apsi-model"` | | `version` | format version, currently `"1"` | | `apsi` | Apsi version that wrote the file | | `units` | JSON: tensor name → dimension vector | | `qtensors` | JSON: tensor name → format, logical shape, axis, packing, scale shape, unit | | `metadata` | JSON: your `metadata=` record | | `sha256` | SHA-256 of the canonical tensor table followed by the data | | `shard` | `"i/N"` in sharded checkpoints | Tensor names are the parameter paths joined with dots (`layers.0.W`), so the tree is rebuilt from the names. ## 2. GGUF ```apsi use models save_gguf("model.gguf", params, metadata=record(llama_context_length=4096), dtype="q8_0") let m = load_gguf("model.gguf") # record(params=…, metadata=…) print(gguf_info("model.gguf")) ``` - GGUF version 3 with 32-byte alignment. Metadata of every GGUF type is supported (integers, floats, booleans, strings and arrays). - `dtype` is `"f32"`, `"f16"` (default), `"bf16"`, `"q8_0"` or `"q4_0"`. Q8_0 and Q4_0 are the block formats of ggml and are **bit-identical** to the reference implementation (tested against the official `gguf` package). Tensors that cannot be block-quantized (1-D tensors, or rows that are not a multiple of 32) are written as F16 or F32. Integer tensors keep their integer type. - Loading memory-maps F32, F16, F64 and integer tensors and decodes BF16, Q8_0 and Q4_0 to f32. - Units are stored as `apsi.units.` keys, so Apsi gets them back and other readers ignore them. - Name the keys the way your runtime expects (for example `general.architecture`, `llama.block_count`). `save_gguf(..., architecture="llama")` sets `general.architecture`. ## 3. Command line ``` apsi model info model.gguf # tensors, types, metadata (headers only) apsi model info model.apsim.index.json apsi model verify model.apsim # check SHA-256 of every shard apsi model verify model.apsim --key "team secret" # and the signature apsi model convert model.safetensors model.gguf --dtype q4_0 apsi model convert model.gguf model.apsim --shard 2GB --key "team secret" apsi model convert model.apsim model.safetensors ``` `convert` reads `.safetensors`, `.gguf` and `.apsim` (including sharded ones) and writes any of the three. Compressed tensors are expanded to f32 when the target format cannot hold them. ## 4. Choosing a format | Need | Use | |---|---| | Resume training, keep units, compressed weights and the tree; detect tampering | `.apsim` | | Run the model in llama.cpp, Ollama or LM Studio | GGUF | | Hand weights to PyTorch, JAX or the Hugging Face Hub | safetensors, or `.apsim` (it is safetensors) | | Checkpoints of 70B–2T parameters | `.apsim` with sharding (and `apsi plan` for the hardware) | # Planning large models: `apsi plan` and `use scale` Training a 21B, 500B, 1T or 2T-parameter model is decided by arithmetic before any code runs: how much memory each GPU needs, how the model must be split, how many GPUs, and how many days. `apsi plan` does that arithmetic. ```text $ apsi plan 1T --active 32B --tokens 15T --precision fp8 Training plan: 1T params (32B active), 15T tokens: 520 x H100 (65 nodes), 8x1x65 TPxPPxDP, none recompute, 68 GB/GPU, 89.3 days at 40% MFU Model 1T parameters, 32B active per token (MoE) Data 15T tokens (~55,879 GB as 32-bit token ids), 3,576,279 steps of 4.19M tokens Hardware 520 x H100 (80 GB) in 65 node(s) Layout TP 8 x PP 1 x DP 65 (tensor x pipeline x data parallel), ZeRO-3/FSDP sharding Memory/GPU 68 GB (model states in total: 14,901 GB) Duration 89.3 days Serving 931 GB weights in fp8 + KV cache -> 13 x H100 ``` | Option | Meaning | |---|---| | `PARAMS` | total parameters: `21B`, `500B`, `1T`, `2T`, … | | `--active N` | active parameters per token for mixture-of-experts models (compute uses these, memory uses all) | | `--tokens N` | training tokens (default 20 per active parameter, the Chinchilla-optimal amount) | | `--gpu NAME` | `H100` (default), `H200`, `B200`, `GB200`, `A100`, `A100-40`, `L40S`, `MI300X`, `MI325X`, `RTX5090`, `RTX4090`, `RTX3090`, `GTX1660` | | `--gpus N` / `--days D` | the GPUs you have, or a target duration (default 90 days) | | `--precision bf16\|fp8`, `--optimizer adamw\|adam8bit\|sgd`, `--seq`, `--layers`, `--width`, `--mfu` | the rest of the setup | The same from a program: `use scale`, then `plan("2T", gpu="B200", precision="fp8")`, `inference("70B", precision="fp8")`, `gpu_table()`. ## How it is computed - **Compute:** 6·N·D + 12·L·d·s·D FLOPs (N active parameters, D tokens, L layers, d width, s sequence length), divided by GPUs × peak × MFU (40 % by default, typical of well-tuned large runs). - **Model states:** 16 bytes per parameter with mixed-precision AdamW (BF16 weights and gradients, FP32 master weights and two moments), sharded over every GPU (ZeRO-3 / FSDP); 10 bytes with 8-bit Adam. - **Activations** (Korthikanti et al. 2022): per layer and micro-batch s·b·d·(34 + 5·a·s/d) bytes without recomputation, 34·s·b·d with selective and 2·s·b·d with full recomputation, divided by tensor parallelism. - **Layout:** the planner tries every tensor-parallel degree (within one NVLink domain), pipeline depth and recomputation level, keeps those that fit in 90 % of GPU memory and whose data parallelism is not wider than the batch, and picks the one that meets the target with the fewest GPUs, counting recomputation and pipeline idle time. - **Honest limits:** clusters are capped at 200,000 GPUs (`max_gpus=`); when a target cannot be met the plan says so and gives the fastest layout. Real runs differ with the framework and the network. ## What the numbers say | Model | Plan (H100 unless noted, 90-day target) | |---|---| | 21B dense, 420B tokens | 20 GPUs, about 88 days | | 70B dense, 1.4T tokens | 208 GPUs (8 × 26), about 89 days | | 1T MoE, 32B active, 15T tokens, FP8 | 520 GPUs, about 89 days | | 2T MoE, 64B active, 1.28T tokens, B200 FP8 | 226 B200, about 15 days | | 2T dense, 40T tokens, B200 FP8 | 31,248 B200, about 103 days | | 2T dense, 40T tokens | needs a ~200,000-GPU cluster for ~500 days: this is why models of this size are mixture-of-experts | Training itself runs on PyTorch (`fit(…, backend="torch")` on one GPU, or `torch_module(model, params)` with PyTorch FSDP / DDP across many GPUs, see [`BACKENDS.md`](/docs/2.4.4/backends)); your own numeric kernels run with `@native` and `@gpu` ([`NATIVE.md`](/docs/2.4.4/native)). # Compute backends: NumPy and PyTorch (CPU, CUDA, ROCm, MPS, XPU) Every model built with [`use layers`](/docs/2.4.4/layers) runs on two backends without changing a line of the model: | Backend | Devices | Number formats | When to use it | |---|---|---|---| | **NumPy** (default) | every CPU core through multi-threaded BLAS | float64 (exact reference results) | always available; physics-grade accuracy, small and medium models, reproducible tests | | **PyTorch** | CPU, NVIDIA CUDA, AMD ROCm, Apple MPS, Intel XPU | fp64, fp32, tf32, bf16 and fp16 mixed precision | large models and data sets, GPUs, cuDNN, fused (flash) attention, `torch.compile` | The PyTorch backend is not a separate model format: it executes the *same* layers with torch kernels and torch autograd, and gives the trained parameters back as an ordinary Apsi parameter tree. A model trained on a GPU can be evaluated on NumPy, saved with `save_params`, served by `use web`, or handed to PyTorch as a real `torch.nn.Module`. - [1. Setup](#1-setup) - [2. Training on PyTorch](#2-training-on-pytorch) - [3. Devices](#3-devices) - [4. Precision and speed](#4-precision-and-speed) - [5. Real torch.nn.Module objects](#5-real-torchnnmodule-objects) - [6. Checking the backend on your machine](#6-checking-the-backend-on-your-machine) - [7. What runs where](#7-what-runs-where) - [8. Troubleshooting](#8-troubleshooting) --- > **Your own loops on the GPU:** this page is about training `use layers` models on PyTorch. To run your own > numeric loops as GPU kernels (one thread per iteration, several GPUs, no CUDA toolkit), use `@gpu` with a > `parallel for` — see [`NATIVE.md`](/docs/2.4.4/native#gpu). ## 1. Setup 1. Install **one** PyTorch build into the Python that runs Apsi. A CUDA build also runs on the CPU, so with an NVIDIA card the CUDA build is all you need. The simplest way picks it for you: ```bash apsi pkg install torch-auto # reads the NVIDIA driver version (nvidia-smi) and installs the matching build ``` (`apsi pkg install ai` does the same together with ONNX Runtime and Transformers, and the setup wizard's *PyTorch* option uses it too.) Or choose the build yourself: | Your hardware | Command | Build | |---|---|---| | NVIDIA GTX 16 / RTX 20, 30, 40 series, driver 560 or newer | `apsi pkg install torch-cuda` | PyTorch ≥ 2.6 + CUDA 12.6 | | NVIDIA RTX 50 series (or driver 570+) | `apsi pkg install torch-cu128` | PyTorch ≥ 2.7 + CUDA 12.8 | | NVIDIA with an older driver (550+) | `apsi pkg install torch-cu124` | PyTorch 2.3–2.6 + CUDA 12.4 | | NVIDIA GTX 10 series or driver 520+ | `apsi pkg install torch-cu118` | PyTorch + CUDA 11.8 | | no NVIDIA GPU (CPU, or Apple silicon on macOS) | `apsi pkg install torch` | CPU build (MPS is included on macOS) | | AMD GPU (Linux, ROCm) | `apsi pkg install torch --index-url https://download.pytorch.org/whl/rocm6.2` | ROCm build | `nvidia-smi` shows your driver version. `apsi pkg install ai-gpu` installs the CUDA 12.6 build together with ONNX Runtime GPU, CuPy and Transformers. - **Already installed?** Any PyTorch 2.3 or newer already present in Apsi's Python is used as it is — nothing is downloaded again (`apsi pkg list` shows the installed build, e.g. `2.6.0+cu126`). Installing another build replaces the old one, so there is never more than one. - pip keeps downloaded wheels in its cache, so a build you downloaded before for the **same Python version** is reused instead of downloaded again. - The Apsi installer has its own Python (in `%LOCALAPPDATA%\Programs\Apsi\runtime` on Windows); a PyTorch that you installed into another Python is not visible to it. To use that Python and its PyTorch instead, run Apsi from the source folder with it: `python -m apsi run --allow-python app.apsi`. 2. Run programs that use the torch backend with the **`python` permission**, because PyTorch is Python code that runs outside Apsi's sandbox: ```bash apsi run --allow-python train.apsi apsi --allow-python train.apsi # the same, short form ``` In a project, put it in `apsi.toml` once: `[run] allow = ["python"]`. 3. Check what Apsi sees: `apsi --allow-python -e "use layers; devices()"` (a list of records `name`, `kind`, `detail`). Without the permission `backend="torch"` stops with a clear message, and `backend="auto"` quietly stays on NumPy. ## 2. Training on PyTorch Add `backend="torch"` to `fit` (and optionally to `predict`): ```apsi use layers use nn let lm = transformer_lm(vocab, d=256, heads=8, layers=6, context=256, dropout=0.1) let r = fit(lm, lm.init(seed=0), inputs, targets, loss="cross_entropy", opt=adamw(lr=3e-4, weight_decay=0.1), epochs=10, batch_size=64, clip=1.0, schedule=fn(s) => cosine_lr(s, 20000, base=3e-4, warmup=500), backend="torch", device="cuda", precision="bf16", compile=true) print("trained on", r.device, "· last loss", r.history[-1]) save_params("lm.npz", r.params) # r.params is a normal Apsi parameter tree let logits = predict(lm, r.params, prompt, backend="torch", device="cuda") ``` Every `fit` option works the same on both backends — `loss`, `opt`, `epochs`, `batch_size`, `seed`, `clip`, `schedule`, `graph`, `mask`, `val`, `verbose` — plus three torch-only options: | Option | Values | Default | |---|---|---| | `device` | `"auto"` (CUDA/ROCm → MPS → XPU → CPU), `"cpu"`, `"cuda"`, `"cuda:1"`, `"mps"`, `"xpu"` | `"auto"` | | `precision` | `"fp64"`, `"fp32"`, `"tf32"`, `"bf16"`, `"fp16"` | `"fp32"` | | `compile` | `true` compiles the forward pass with `torch.compile` (falls back to eager mode, with a note, if the compiler is not available) | `false` | `backend="auto"` uses PyTorch when it is installed and permitted, NumPy otherwise — handy for programs that should run everywhere. How the optimizer is mapped: `sgd(lr, momentum)` → `torch.optim.SGD`, `adam(lr, b1, b2, eps)` → `torch.optim.Adam`, `adamw(…, weight_decay)` → `torch.optim.AdamW` with the same hyper-parameters. On the torch backend `opt` may also be a name: `"rmsprop"`, `"adagrad"`, `"nadam"`, `"radam"` (PyTorch defaults; learning rate via `schedule`). Losses: `"mse"`, `"mae"`, `"huber"`, `"cross_entropy"` (logits of any shape `[…, classes]` with integer labels), `"bce"` (numerically stable, from logits) and `"vae"`. A loss written in Apsi works on the NumPy backend; on the torch backend use a named loss. ## 3. Devices `devices()` lists what this machine offers, one record per device: ```apsi for d in devices(): print(d.name, d.kind, d.detail) ``` ```text numpy cpu NumPy 2.1.3, float64, 16 threads cpu cpu PyTorch 2.5.1, 16 threads cuda:0 cuda NVIDIA GeForce RTX 4090, 23.6 GiB, bf16 yes ``` AMD GPUs with the ROCm build of PyTorch appear as `cuda:N` with kind `rocm` (that is how PyTorch names them); Apple-silicon GPUs as `mps`; Intel GPUs as `xpu:N`. ## 4. Precision and speed | `precision` | Weights stored as | Arithmetic | Notes | |---|---|---|---| | `fp64` | float64 | float64 | bit-for-bit comparable with the NumPy backend; slow on consumer GPUs; not on MPS | | `fp32` | float32 | float32 | the default | | `tf32` | float32 | TF32 tensor cores for matrix products (NVIDIA Ampere and newer) | ~3–8× faster matmuls, ~1e-3 relative error | | `bf16` | float32 | bfloat16 autocast (mixed precision) | the usual choice for training on modern GPUs; no loss scaling needed | | `fp16` | float32 | float16 autocast with dynamic loss scaling (`GradScaler`) | for GPUs without bf16 | What makes the torch backend fast: - the training data is moved to the device once; mini-batches are sliced there (no copy per step) - the loss is synchronised with the CPU only once per epoch - attention uses `torch.nn.functional.scaled_dot_product_attention` — Flash/memory-efficient kernels on GPUs - `lstm_layer` runs as a single cuDNN call on CUDA/XPU (same weights, mapped onto `torch.nn.LSTM`); on the CPU the input projection for all time steps is computed in one matrix product - graph layers use `index_add` / `scatter_reduce`, which run in parallel on the GPU - `compile=true` fuses element-wise operations with `torch.compile` For multi-GPU training (DDP/FSDP), ONNX export or TorchScript, take the model to PyTorch as a module (next section) and use PyTorch's own tools. ## 5. Real `torch.nn.Module` objects ```apsi use layers let torch = pyimport("torch") let net = resnet_mlp(16, 4, width=128, blocks=4) let m = torch_module(net, net.init(seed=0), device="cuda") # a torch.nn.Module print(m) # ApsiModule(…, the flat parameter list) let y = m(torch.randn(8, 16).cuda()) # forward pass in PyTorch # … train it with any PyTorch code: torch.optim, DDP, Lightning, … let params = params_from_torch(net, m) # back to an Apsi parameter tree ``` The module holds every weight as a `torch.nn.Parameter` (`m.parameters()`, `m.state_dict()`, `m.to(…)`, `m.train()` / `m.eval()` all behave as usual) and its `forward(x, graph=…, mask=…)` runs the Apsi model on torch tensors. The parameter order follows the Apsi parameter tree, so `params_from_torch` always maps the weights back to the right places. ## 6. Checking the backend on your machine `check_torch()` runs every layer type — dense, convolution, pooling, batch/layer/RMS norm, LSTM, GRU, attention, transformers, residual/parallel/DAG composition, siamese weight sharing, GCN, GraphSAGE, GAT, GIN, VAE — on NumPy and on PyTorch in float64 and compares the outputs **and the gradients**. It uses the GPU when there is one (`device="auto"`); pass `device="cpu"` to check the CPU: ```bash apsi --allow-python -e "use layers; check_torch()" ``` In PowerShell, put code that contains double quotes inside single quotes: `apsi --allow-python -e 'use layers; check_torch(device="cpu")'`. ```text mlp output 2.22e-16 gradient 4.44e-16 lstm output 5.55e-17 gradient 3.33e-16 gat output 1.11e-16 gradient 8.88e-16 … OK: largest difference 1.78e-15 on cpu ``` The project's test suite runs the same check: `python -m unittest tests.test_torch` (skipped when PyTorch is not installed). ## 7. What runs where | Feature | NumPy | PyTorch | |---|---|---| | every layer, composition and zoo model of `use layers` | ✓ | ✓ | | `lambda_layer(fn)` and `activation(fn)` with an Apsi function | ✓ | named activations only; a lambda layer needs a torch version (`lambda_layer(fn, torch_fn=…)` from Python) | | a loss written in Apsi | ✓ | use a named loss | | units of measurement on tensors | ✓ | values are converted to plain numbers (SI) | | exact float64 reference results | ✓ | with `precision="fp64"` | | GPUs, mixed precision, `torch.compile` | — | ✓ | | `grad`, `value_and_grad`, `hessian` on your own functions | ✓ | (inside `fit` only) | ## 8. Troubleshooting | Message | Fix | |---|---| | `fit(backend="torch") needs permission 'python'` | run with `--allow-python` (or `[run] allow = ["python"]` in `apsi.toml`) | | `backend="torch" needs PyTorch` | `pip install torch` into the Python that runs Apsi (`apsi doctor` shows its version) | | `device="cuda": PyTorch sees no CUDA/ROCm GPU` | install the GPU build of PyTorch and a current driver, or use `device="auto"` | | `precision="fp64" is not supported on Apple MPS` | use `fp32` on MPS | | `the torch backend has no implementation of …` | a custom layer class without `torch_apply`; use `backend="numpy"` for it | | `torch.compile failed (…); continuing eagerly` | not an error — the compiler (e.g. a C++ toolchain for Inductor) is missing; training continues without it | | results differ slightly between backends | expected with fp32/bf16 (rounding); `precision="fp64"` matches NumPy to ~1e-15 | # Fewer tokens, nothing lost (`use tokens`) Language models charge by the token and read slower the more they get. `use tokens` removes redundancy from what you send — **with transforms that are exactly reversible**, so no information is summarised, dropped or guessed: | transform | reverse | what it removes | measured | |---|---|---|---| | `pack(value)` | `unpack(text)` | repeated JSON keys, quotes and spacing: a list of records becomes a table with the keys once and one compact line per record | 200 orders: pretty JSON 9,802 → 3,625 tokens (**63 % fewer**); minified JSON 8,401 → 3,625 (57 %) | | `dedupe(text)` | `expand(text)` | lines that repeat: written once in a header, referenced as `§N` | a 500-line log: 5,689 → 2,339 (**59 %**) | | `diff(old, new)` | `patch(old, delta)` | resending a whole document when a few lines changed | a 5,951-token document with two edits: 19 tokens (**99.7 %**) | | `best(value)` | `restore(text, method)` | tries the transforms that apply and keeps the smallest | | ```apsi use tokens let rows = load_json("orders.json") let text = tokens.pack(rows) print(tokens.savings(json_str(rows, indent=2), text)) # 9,802 → 3,625 tokens: 63.0 % saved (2.70× fewer) assert tokens.unpack(text) == rows # exact, always let prompt = "Orders as a table (first line = column names, one JSON-encoded row per line):\n" + text ``` `tokens.count(text)` is exact when `tiktoken` is installed (`apsi pkg install tiktoken`) and a close estimate (±10–15 %) otherwise; the measurements above are estimates. ## What to expect Savings depend on how much redundancy the data has — there is no transform that shrinks everything: * **Large:** lists of records with the same fields (API responses, database rows, logs as JSON), logs and transcripts with repeated lines, documents sent again after small edits. * **None:** prose, source code without repetition, records whose fields differ from one to the next. `best()` then returns the input unchanged, so it never costs extra. Models read the packed forms well when the prompt says what they are (one sentence, as above). `diff` is meant for a model that already has the previous version — in the conversation or in a provider's prompt cache. ## Why use it * **Lower cost, same answer.** You pay for every input token. A packed list of 200 records costs about a third of the pretty-printed JSON, and the model receives exactly the same data. * **More fits in the context window.** A 128k-token window holds about 2.7 times as many packed records. * **Faster replies.** Time to first token grows with the prompt length; a shorter prompt starts answering sooner. * **Safe by construction.** Each transform has an exact inverse, and the test suite checks `unpack(pack(x)) == x`, `expand(dedupe(t)) == t` and `patch(old, diff(old, new)) == new` on random data. There is no setting that trades accuracy for size. ## Reference | Function | Returns | What it does | |---|---|---| | `count(text, model="gpt-4o")` | `Int` | Tokens in `text` (a non-string value is counted as minified JSON). Exact when `tiktoken` is installed, otherwise an estimate. `model` picks the tiktoken encoding; unknown names use `o200k_base`. | | `exact()` | `Bool` | `true` when `count()` is exact. | | `savings(before, after, model=)` | `Savings` | Counts both texts. Fields: `before`, `after`, `saved`, `percent`, `ratio`. Prints as `9,802 → 3,625 tokens: 63.0 % saved (2.70× fewer)`. | | `pack(value)` | `Str` | A list of two or more records with the same keys in the same order becomes a table: `@table ["id","name"]` followed by one line per record (`1,"ann"`). Any other JSON value becomes minified JSON. | | `unpack(text)` | value | The exact inverse of `pack`. | | `pack_jsonl(text)` | `Str` | JSON Lines (one object per line) → `pack` of the records. Unpacks to the list of parsed objects. | | `dedupe(text, min_chars=24, min_count=2)` | `Str` | Lines of at least `min_chars` characters that occur at least `min_count` times are written once in an `@refs N` header and replaced by `§1`, `§2` … where they occur. A `§` already in the text is escaped as `§§`. Text with no repeats becomes `@plain` + the text. | | `expand(text)` | `Str` | The exact inverse of `dedupe`. | | `diff(old, new)` | `Str` | Line operations that turn `old` into `new`: `=N` keep N lines, `-N` drop N lines, `+text` insert a line. | | `patch(old, delta)` | `Str` | Applies a `diff` result. | | `best(value, model=)` | `(Str, Str)` | Tries the transforms that apply (`plain`, `dedupe` for text; `pack`, `pack+dedupe` for other values) and returns the smallest text and the method name. | | `restore(text, method)` | value | The inverse of `best`, given its method name. | ### The packed table format ```text @table ["id","customer","total","paid"] 1,"ann",12.5,true 2,"bob",8,false 3,"cid",null,true ``` The first line holds the column names as a JSON array. Every following line is the values of one record, JSON-encoded and separated by commas, without the surrounding brackets. Strings keep their JSON quotes and escapes, so a comma or a line break inside a value cannot be confused with a separator. Nested values (lists, objects) stay as JSON inside the row. Records are packed as a table only when every record has the same keys **in the same order**; otherwise the whole value is minified JSON, which is still smaller than pretty-printed JSON and still exact. To get the table form for data whose key order varies, put the keys in one order first: ```apsi let keys = ["id", "customer", "total"] let rows2 = [{k: r.get(k) for k in keys} for r in rows] ``` ## Recipes **Records into a prompt** ```apsi use tokens let rows = db.query("select id, customer, total from orders where day = ?", today) let prompt = "Orders (first line: column names; then one JSON row per line):\n" + tokens.pack(rows) + "\n\nWhich customers spent more than 100 in total?" ``` **A long log** ```apsi let log = read_text("server.log") let short = tokens.dedupe(log, min_chars=30) print(tokens.savings(log, short)) let prompt = "Server log. Lines starting with § refer to the numbered lines after @refs.\n" + short ``` **A document that changes while you talk about it** ```apsi var doc = read_text("spec.md") # … the model has already seen doc earlier in the conversation … let edited = read_text("spec.md") let prompt = "The spec changed. Line operations against the previous version " + "(=N keep, -N delete, +text insert):\n" + tokens.diff(doc, edited) doc = edited ``` **Let Apsi choose** ```apsi let text, method = tokens.best(payload) send(text) # and keep method to restore: tokens.restore(text, method) ``` ## Limits * Token counts without `tiktoken` are estimates; the savings percentage is reliable to a few points because both sides are estimated the same way. * Packing changes nothing for data with no repetition; `best()` then returns the smallest form, never a larger one. * A model must be told the format in one sentence (examples above). Without that sentence it may misread `§N` or `=N` lines. # Hugging Face models without Python (`use hub`) `use hub` downloads models from the Hugging Face Hub, tokenizes text and **runs the models natively** — embeddings, classification, named entities, fill-mask and text generation — with Apsi's own code. You do not need Python's `transformers`, `torch`, `huggingface_hub` or `tokenizers` packages, and you do not need `--allow-python`. ```apsi use hub let sentiment = hub.pipeline("sentiment-analysis", "distilbert/distilbert-base-uncased-finetuned-sst-2-english") print(sentiment("Apsi is a joy to use")) # {"label": "POSITIVE", "score": 0.99…} let st = hub.model("sentence-transformers/all-MiniLM-L6-v2") print(st.similarity("A cat sits on the mat", "A kitten is on the rug")) let vectors = st.embed(["first text", "second text"]) # [2, 384], normalised like sentence-transformers let lm = hub.model("HuggingFaceTB/SmolLM2-135M") print(lm.generate("The three laws of motion are", max_new_tokens=40)) lm.generate("Once upon a time", max_new_tokens=60, temperature=0.8, top_p=0.95, stream=fn(t) => print(t, end="")) ``` ```apsi use hub print(hub.search("sentence similarity", task="sentence-similarity", limit=5)) print(hub.files("sentence-transformers/all-MiniLM-L6-v2")) let cfg = hub.config("sentence-transformers/all-MiniLM-L6-v2") # config.json as a dict let w = hub.load("sentence-transformers/all-MiniLM-L6-v2") # {name: tensor} from safetensors print(w["embeddings.word_embeddings.weight"].shape) let tok = hub.tokenizer("sentence-transformers/all-MiniLM-L6-v2") # tokenizer.json, natively let e = tok.encode("Apsi reads like Python.") print(e.tokens, e.ids, e.attention_mask) print(tok.decode(e.ids)) ``` Run it with `apsi run app.apsi --allow-net=huggingface.co`. Downloads of large files are redirected by the Hub to its storage hosts; the permission for `huggingface.co` covers them. ## Functions | | | |---|---| | `hub.search(query, task=, limit=20, sort="downloads")` | models matching a query: id, task, downloads, likes | | `hub.info(repo, revision=, kind="model")` | the Hub's metadata for a model, dataset (`kind="dataset"`) or Space | | `hub.files(repo, revision=)` | the files in the repository | | `hub.download(repo, file, revision="main")` | one file → its local path | | `hub.snapshot(repo, patterns=["*.json", "*.safetensors", "*.txt", "*.model"])` | the matching files → a folder | | `hub.config(repo)` | `config.json` as a dict | | `hub.load(repo)` / `hub.load(repo, "file.gguf")` | weights as `{name: tensor}`: `model.safetensors`, sharded safetensors (`model.safetensors.index.json`), or a GGUF file | | `hub.tokenizer(repo)` / `hub.tokenizer_from_file(path)` | a tokenizer from `tokenizer.json` | | `hub.model(repo)` / `hub.model_from_folder(path)` | a model that runs natively (below) | | `hub.pipeline(task, repo)` | a function for one task: `"sentiment-analysis"` / `"text-classification"`, `"feature-extraction"` / `"sentence-similarity"`, `"ner"` / `"token-classification"`, `"fill-mask"`, `"text-generation"` | | `hub.login(token)` / `hub.logout()` | a Hub access token for private and gated models | | `hub.offline(true)` | use only the local cache | | `hub.cache_dir()` / `hub.clear_cache(repo)` | where files are kept / remove them | ## Tokens, private and gated models ```apsi use env hub.login(env.secret("HF_TOKEN")) # or set HF_TOKEN in the environment, or pass token= to a call let w = hub.load("my-org/private-model") ``` * The token is sent only to the Hub's own address, never to the storage host a download is redirected to. * Gated models (Llama, Gemma …) need you to accept their terms on the model page once; after that the token works. * `HF_ENDPOINT` points the client at a mirror or a self-hosted Hub. ## Cache, resuming and checking * Files are kept in `~/.apsi/hub` (or `APSI_HUB_CACHE`), one folder per commit, so a model that changes on the Hub never mixes old and new files. * An interrupted download continues where it stopped the next time you ask for the file. * Large files are checked against the SHA-256 the Hub publishes; a file that does not match is deleted, never used. * `hub.offline(true)` (or `APSI_HUB_OFFLINE=1`) answers from the cache only — for machines without internet. ## Tokenizers `tokenizer.json` files of the Hugging Face `tokenizers` format are read natively: | Family | Examples | Supported | |---|---|---| | WordPiece with the BERT normaliser | BERT, DistilBERT, MiniLM, E5, BGE | yes | | Byte-level BPE | GPT-2, RoBERTa, Llama 3, Qwen, Mistral Nemo | yes | | SentencePiece-style BPE with byte fallback | Llama 2, Mistral 7B, Gemma | yes | | Unigram (SentencePiece) | T5, ALBERT, XLM-R, mBART | yes (the `Precompiled` normaliser is applied as NFKC) | `encode(text, pair=none, add_special_tokens=true)` returns `ids`, `tokens` and `attention_mask`; special tokens are added by the tokenizer's own post-processor template. `decode(ids, skip_special_tokens=true)` turns ids back into text. Pre-tokenizer patterns written with `\p{L}` / `\p{N}` (Llama 3, Qwen) are translated to Python's regular expressions; letters of the common scripts (Latin, Greek, Cyrillic, Arabic, Hebrew, Devanagari, Thai, Hangul, kana, CJK) are covered, so very rare scripts can split differently from the Rust `tokenizers` library. ## Running models natively `hub.model(repo)` downloads `config.json`, the safetensors weights (also sharded; F32, F16 and BF16), the tokenizer and, for sentence-transformers models, their pooling settings, then runs the network with NumPy. | Family (`model_type`) | Examples | Methods | |---|---|---| | `bert`, `distilbert`, `roberta`, `xlm-roberta`, `camembert` | BERT, DistilBERT, RoBERTa, XLM-R, MiniLM, E5, BGE, GTE and other sentence-transformers built on them | `embed`, `similarity`, `classify`, `tokens`, `fill_mask`, `hidden` | | `gpt2` | GPT-2, DistilGPT-2 | `generate` | | `llama`, `mistral`, `qwen2`, `qwen3`, `gemma` | Llama 2 / 3 (with Llama 3 RoPE scaling), TinyLlama, SmolLM, Mistral, Qwen 2 / 2.5 / 3, Gemma | `generate` | * **`embed(texts, normalize=)`** — sentence vectors with the model's own pooling (mean, CLS or max) and, when the model says so, L2 normalisation, as sentence-transformers does. **`similarity(a, b)`** — cosine similarity. * **`classify(texts, top_k=1)`** — labels from the checkpoint's own head and `id2label` (softmax, or sigmoid for multi-label models). * **`tokens(text)`** — one label per token (named-entity models). **`fill_mask(text, top_k=5)`** — the best words for `[MASK]` / ``. * **`generate(prompt, max_new_tokens=64, temperature=0, top_k=0, top_p=1, repetition_penalty=1, seed=, stream=)`** — text generation with a key/value cache. `temperature=0` is greedy. It stops at the model's end-of-sequence token (from `generation_config.json` or `config.json`). `stream` receives each new piece of text as it appears. * **`hidden(texts)`** — the last hidden states, for your own heads. How it is checked: tiny checkpoints written in the exact Hugging Face file layout are run in the test suite. The encoder is compared with an independent per-head reference implementation, padding is checked not to change results, and cached generation is compared with recomputing the whole sequence at every step. On a machine with `transformers` and `torch`, `APSI_HF_COMPARE=1 python -m unittest tests.test_hub_models` also compares against the reference library. ### Methods and what they return | Call | Input | Returns | |---|---|---| | `m.embed("text")` / `m.embed(["a", "b"])` | one text or a list | `Tensor[H]` / `Tensor[B, H]` (float32) | | `m.similarity(a, b)` | two texts | `Float` in −1 … 1 (cosine of the normalised embeddings) | | `m.classify("text", top_k=1)` | one text or a list | `{"label": "POSITIVE", "score": 0.998}`; with `top_k > 1` a list of those, best first; for a list of texts, one entry per text | | `m.tokens("text")` | one text | `[{"token": "Paris", "label": "B-LOC", "score": 0.99}, …]` — special tokens are left out; sub-word pieces keep the tokenizer's spelling (`##ton`, `Ġworld`) | | `m.fill_mask("Paris is the [MASK] of France.", top_k=5)` | one text with the model's mask token | `[{"token": "capital", "id": 3007, "score": 0.97}, …]` | | `m.generate(prompt, …)` | a text, or a list of token ids | the generated continuation as text (without the prompt) | | `m.hidden(texts)` | one text or a list | `Tensor[T, H]` / `Tensor[B, T, H]`, padding included (positions past each text are padding) | | `m.tokenizer`, `m.config`, `m.labels` | | the tokenizer, `config.json` as a dict, `{id: label}` | Texts longer than the model's `max_position_embeddings` are cut at that length. Lists are padded to the longest text and processed as one batch, which is several times faster than one call per text. `generate` options: | Option | Default | Meaning | |---|---|---| | `max_new_tokens` | 64 | stop after this many new tokens | | `temperature` | 0 | 0 = always the most likely token (deterministic); 0.7–1.0 for varied text | | `top_k` | 0 (off) | sample only from the k most likely tokens | | `top_p` | 1.0 (off) | sample from the smallest set of tokens whose probabilities add up to p | | `repetition_penalty` | 1.0 (off) | values like 1.1–1.3 discourage repeating tokens already in the text | | `seed` | none | a number makes sampling repeatable | | `stream` | none | a function that receives each new piece of text as soon as it is decoded | | `add_special_tokens` | true | whether the tokenizer adds its start token (``, `<|begin_of_text|>`) | ### Recipes **Semantic search over your documents** ```apsi use hub let st = hub.model("sentence-transformers/all-MiniLM-L6-v2") let docs = ["How to reset a password", "Shipping times to Europe", "Refunds for damaged items"] let index = st.embed(docs) # [3, 384], normalised let q = st.embed("my parcel arrived broken") let scores = index @ q # cosine similarity, because both are normalised let best = argmax(scores) print(docs[best], scores[best]) # Refunds for damaged items ``` For many documents, embed them in lists of 32–128 and keep the matrix (`save_npy`, or `use memory` for more than fits in RAM). **Classifying a batch** ```apsi let clf = hub.pipeline("sentiment-analysis", "distilbert/distilbert-base-uncased-finetuned-sst-2-english") for r in clf(["great product", "arrived late and broken"]): print(r["label"], round(r["score"], 3)) ``` **Named entities, joined into words** ```apsi let ner = hub.model("dslim/bert-base-NER") var words = [] for t in ner.tokens("Ada Lovelace was born in London"): if t["token"].startswith("##") and len(words) > 0: words[-1]["token"] += t["token"][2:] elif t["label"] != "O": words.append(t) print(words) # Ada (B-PER), Lovelace (I-PER), London (B-LOC) ``` **Streaming text as it is generated** ```apsi let lm = hub.model("Qwen/Qwen2.5-0.5B-Instruct") let prompt = "<|im_start|>user\nName three uses of lasers.<|im_end|>\n<|im_start|>assistant\n" lm.generate(prompt, max_new_tokens=120, temperature=0.7, top_p=0.9, seed=1, stream=fn(t) => print(t, end="")) ``` Chat models expect their own prompt format (above: Qwen's). The format is in the model card and in `tokenizer_config.json` under `chat_template`. **A model you already have on disk** (downloaded elsewhere, or your own fine-tune saved by `transformers`) ```apsi let m = hub.model_from_folder("models/my-classifier") # config.json, model.safetensors, tokenizer.json ``` ### Errors you may see | Message | Meaning and fix | |---|---| | a permission error naming `huggingface.co` | run with `--allow-net=huggingface.co`, or `hub.offline(true)` after a first download | | `401` / `403` for a model | private or gated: `hub.login(token)` and accept the model's terms on its page | | `model type 'phi3' does not run natively yet` | see "Not native yet" below; use `hf_pipeline` with `--allow-python` | | `embed() needs an encoder model` | `embed`, `classify`, `tokens`, `fill_mask` are for encoders; `generate` for decoders | | `the text has no mask token` | write the model's mask token: `[MASK]` (BERT) or `` (RoBERTa, XLM-R) | | `… has no safetensors or GGUF weights (only PyTorch .bin files …)` | look for a safetensors version of the model, or see the note on `.bin` files below | ### Speed and size Everything runs on the CPU in float32 with NumPy's multi-threaded BLAS. Encoders such as MiniLM and BERT-base handle sentences in milliseconds; decoders up to about 1–2 billion parameters generate at interactive speed on a modern CPU (a 1.1B model needs about 4.4 GB of RAM in float32). For bigger models, use GGUF with llama.cpp, or PyTorch through `hf_pipeline`. ### Not native yet * Gemma 2 and 3 (extra norms and soft-capping), Phi, Falcon, MPT and mixture-of-experts models, T5 and BART (encoder–decoder), vision and audio models. * Chat templates (Jinja in `tokenizer_config.json`): write the prompt in the model's format yourself. * Quantised GGUF formats other than Q8_0 and Q4_0 for `hub.load`. For those, `hf_pipeline(task, model)` in `use models` runs Python's `transformers` (needs `--allow-python`). PyTorch `.bin` checkpoints are pickles and can run code when loaded; `hub.load` refuses them. Download them with `hub.download` and open them with `torch_load` under `--allow-python` only if you trust the publisher. # RAM, SSD, CPU and GPU (`use memory`) Big data and big models run out of memory in the middle of the night. `use memory` decides where data lives so that does not happen: in RAM while it fits, on the SSD (memory-mapped, the same code) when it does not, and on the GPU with the most free memory when there is one. ```apsi use memory print(memory.status()) # RAM 7.3/7.8 GB free (this program 0.04 GB, budget 5.1 GB) | swap 0.0 GB | SSD 29 GB free at ~/.apsi/spill | # 2 cores, load 0.34 | no GPU ``` ## The memory you really have (containers) ```apsi print(memory.limits()) # 5.8 GB usable (container (cgroup v1)), 5.8 GB free, budget 4.0 GB ``` In Docker, Kubernetes or a systemd slice, `/proc/meminfo` shows the whole host, but the kernel kills the process at the container's limit. Apsi reads the limit and usage of the process's memory cgroup (v1 and v2, the smallest limit up the hierarchy) and uses whichever is smaller everywhere: the default budget, `array()`, `status()`, `pressure()`. A program that thought it had a 64 GB host when its pod allows 4 GB no longer plans with 64. ## Memory pressure ```apsi print(memory.pressure()) # ok: 5.8 GB free (99 %), stalls 0.0 % (some) / 0.0 % (full) let w = memory.on_pressure(fn(p) => print("memory is", p.level), level="high") memory.relieve() # give cached RAM back now w.stop() ``` The level is `ok`, `moderate`, `high` or `critical`, from the free fraction of the usable memory and, on Linux, the kernel's pressure stall information (`/proc/pressure/memory`, or the cgroup's own `memory.pressure`): the share of time tasks waited for memory in the last 10 seconds. Apsi reacts by itself — no thread, the level is re-measured at most twice a second on allocation paths: | Level | What Apsi does | |---|---| | high | new big `memory.array()`s go to the SSD; stores halve their RAM tier; pools drop their spare buffers | | critical | as high, and stores empty their RAM tier (values stay on the SSD), then a garbage collection | | back to ok | stores grow their RAM tier back to the size you asked for | `on_pressure(fn, level, every=1, repeat=10)` adds a background watcher that does the same and calls `fn` when the level is reached (again every `repeat` seconds while it lasts) — for example to shrink a batch or pause a producer. ## Reusing big temporaries ```apsi let p = memory.pool() for step in 0..1000: let a = p.take([4096, 8192]) # 256 MB, already mapped after the first step work(a) p.give(a) # do not use a after this ``` Fresh memory costs page faults and zeroing every time it is touched; a pool hands back memory that is already mapped. On a 2-core VM, twenty 256 MB temporaries took 768 ms from a pool and 1,041 ms freshly allocated (1.36×). `take(shape, dtype, zero=true)` clears it; spare buffers are bounded (`max_gb`, default a tenth of the budget) and dropped first under pressure. An array that is never given back is simply freed when it is no longer used. ## What lives where ```apsi for r in memory.report(): print(r) # [200000000, 8] float32 5.960 GB SSD /home/me/.apsi/spill/tmpab12.npy ``` ## Arrays that never run out of RAM ```apsi memory.budget(16) # GB of RAM for big arrays (default: 70 % of what is free) let x = memory.array([200_000_000, 8], "float32") # 6.4 GB: in RAM if it fits the budget, else on the SSD x[0, 0] = 1.5 # the same NumPy array either way print(memory.on_disk(x)) memory.free(x) # RAM back now / the spill file deleted now ``` Spill files live in `~/.apsi/spill` (or `APSI_SPILL_DIR`) and are deleted when the array is freed or the program ends. `APSI_RAM_BUDGET=12GB` sets the budget from outside. ## Out-of-core processing ```apsi var total = 0.0 for block in memory.chunks("features.npy", rows=1_000_000): # the next block is read while this one is used total = total + block.sum() let y = memory.map(fn(b) => b * 2.0 + 1.0, x, rows=500_000) # all cores; output in RAM or on the SSD ``` `chunks` hands out views (no copy). For a file it asks the kernel to read the next block ahead while the current one is used, and releases each finished block from the process, so streaming a file much larger than RAM keeps memory flat: a 3 GB file streamed in 1,000,000-row blocks peaked at 97 MB of RSS (before: 3,103 MB) at the same speed. `map` runs blocks on all cores (NumPy releases the GIL), releases finished input and output blocks the same way, and allocates its output with `memory.array`, so the output can be larger than RAM too. ## A cache with two tiers ```apsi let cache = memory.store("embeddings", ram_mb=512, disk_gb=50) cache.set(doc_id, vector) # RAM (least recently used leave first) and the SSD let v = cache.get(doc_id) # RAM hit, or read back from the SSD (arrays come back memory-mapped) print(cache) # @memory.cached # results survive restarts fn tokenize(path): pass ``` * Arrays are stored as `.npy` files and come back memory-mapped (no copy until you touch them). * Other values are pickled and **signed** with this user's key; a file changed outside Apsi is never unpickled. * The SSD tier is bounded (`disk_gb`); the least recently used files are removed first, down to 90 % of the limit. Its size is kept as a running total, so `set` costs the same with 100 or 100,000 entries (before, every `set` listed the whole folder: 8.6 ms per `set` at 8,000 entries, now 0.07 ms). * A store name is a folder under `~/.apsi/store`. A store at a path you choose (`memory.store("/data/cache")`) needs `--allow-write` for that path. ## Batches that fit ```apsi let loss, batch = memory.fit_batch(fn(b) => train_step(b), 512) ``` `fit_batch` calls the function and, on out-of-memory (CPU `MemoryError`, CUDA out-of-memory from PyTorch or CuPy), empties the GPU cache, halves the batch and tries again. It returns the result and the batch size that worked, so the next call can start there. ## Devices ```apsi print(memory.devices()) # cpu (cores, free RAM) and every GPU with its free memory let dev = memory.best_device(8) # "cuda:1" if a GPU has 8 GB free, otherwise "cpu" ``` GPU memory is read through PyTorch or CuPy when either is installed. Everything else works without them. # Real time: 1000–2000 Hz loops and video (`use rt`) `use rt` runs work **on time**: control loops at 1–2 kHz with microsecond accuracy, many periodic tasks on one scheduler, and video pipelines from 640×480 to 3840×2160 with the lowest possible latency. It works on the CPU alone and moves frame work to an NVIDIA GPU when you ask for it. ## Loops ```apsi use rt fn control(tick, t): # t = seconds since the start let u = pid.update(sensor.read()) motor.set(u) let s = rt.loop(1000, control, seconds=5) # 1000 times per second print(s) # achieved rate, lateness p50/p99, missed ticks, CPU busy ``` * Waiting is a coarse sleep followed by a short spin on the high-resolution clock, so each tick starts within microseconds of its deadline while the CPU stays mostly idle. * A tick that cannot start on time is **counted and skipped** (`catch_up=true` runs it late instead) — the rhythm never drifts, one slow tick never causes a burst of late ones. * `on_error="skip"` keeps the loop alive when one tick raises; the error is counted and the first one printed. * During the loop the garbage collector is paused and, on Windows, the 1 ms timer resolution and a high thread priority are requested; everything is restored afterwards. * `for tick in rt.ticks(2000, seconds=1):` gives the same timing as a `for` loop. Many tasks at different rates share one loop: ```apsi let s = rt.scheduler() s.every(1ms, read_sensor) # 1000 Hz s.every(10ms, update_model) # 100 Hz s.every(100ms, redraw) # 10 Hz print(s.run(seconds=10)) # per task: runs, worst lateness, errors ``` Measured at 2000 Hz on an AMD EPYC 7763 GitHub runner (no real-time kernel, three 3-second runs): **6,000 of 6,000 ticks, 0 missed, lateness p50 0.2 µs, p99 0.3–0.4 µs, worst single tick 26–48 µs**. On a busy shared 2-core VM 0.1–0.3 % of ticks were skipped when the hypervisor took the CPU away for 1–5 ms. Reproduce with `benchmarks/rt/loop.apsi`. On Linux the loop asks for 1 ns timer slack while it runs, so sleeps end on time. It deliberately does not switch to `SCHED_FIFO`: a spinning real-time thread is throttled by the kernel and then stalls for about 50 ms. ## Video One call runs the whole pipeline: a capture thread, a newest-frame-only hand-off (frames never queue up, so latency cannot grow), your function, timing, adaptive resolution under sustained overload and error isolation. ```apsi use rt let cam = rt.camera(0, "fhd", fps=60) # or rt.synthetic("1600x900", fps=240) for testing fn process(frame, info): let g = rt.gray(frame) if info.prev_gray is not none: let box = rt.motion(info.prev_gray, g) # where something moved, or none pass let report = rt.video(cam, process, fps=90, seconds=30, adapt=true) print(report) # fps, work p50/p99, latency p50/p99, jitter, dropped, errors, scales used ``` ### Frame sizes Any width × height from 16 to 16384 works. Sizes can be given as numbers, `"WIDTHxHEIGHT"`, or a name: | name | size | name | size | |---|---|---|---| | `qvga` | 320×240 | `hd+` / `900p` | 1600×900 | | `vga` | 640×480 | `fhd` / `1080p` | 1920×1080 | | `svga` | 800×600 | `qhd` / `1440p` | 2560×1440 | | `wvga` | 854×480 | `4k` / `uhd` / `2160p` | 3840×2160 | | `xga` | 1024×768 | | | | `hd` / `720p` | 1280×720 | | | | `wxga` | 1366×768 | | | `rt.size("fhd")` → `(1920, 1080)`; `rt.sizes` lists them all. ### Measured Pipeline: gray conversion + motion detection of every frame, CPU only, AMD EPYC 7763 GitHub Actions runner (4 vCPU), synthetic source in the same process (a real camera captures in hardware, so a real machine does better). "Capped" runs at the target frame rate and reports the end-to-end latency (frame captured → your function done) and jitter of the output rhythm. Best of 2 runs. | size | pixels | uncapped fps | work p50 | work p99 | capped fps (target) | latency p50 | jitter | |---|---|---|---|---|---|---|---| | vga | 640×480 | 708 | 1.36 ms | 1.67 ms | 240.0 (240) | 1.82 ms | 0.24 ms | | svga | 800×600 | 506 | 1.92 ms | 2.31 ms | 240.0 (240) | 1.80 ms | 0.29 ms | | hd | 1280×720 | 290 | 3.28 ms | 3.70 ms | 120.0 (120) | 3.45 ms | 0.35 ms | | wxga | 1366×768 | 251 | 3.82 ms | 4.15 ms | 120.0 (120) | 3.86 ms | 0.35 ms | | hd+ | 1600×900 | 169 | 5.62 ms | 6.17 ms | 90.0 (90) | 5.21 ms | 0.86 ms | | fhd | 1920×1080 | 106 | 8.84 ms | 9.77 ms | 90.0 (90) | 7.61 ms | 0.53 ms | | qhd | 2560×1440 | 50 | 19.28 ms | 21.68 ms | 40.0 (60) | 19.77 ms | 2.86 ms | | 4k | 3840×2160 | 20 | 46.61 ms | 51.37 ms | 20.0 (30) | 38.50 ms | 4.55 ms | Every size up to 1920×1080 holds its target; 2560×1440 reaches 57 of 60 fps and 3840×2160 20 of 30 fps here, because the frame work alone takes longer than the frame time on this processor (a faster CPU or `@native` work functions reach the higher targets). Reproduce with `benchmarks/rt/sizes.apsi`. ### Staying smooth under load * **Newest frame only.** If your function takes longer than a frame, the frames that arrived meanwhile are dropped and counted; the next call gets the newest one. Latency stays one frame, never a growing queue. * **`adapt=true`.** When the work no longer fits the frame budget (p95 of the last 30 frames), the working resolution steps down (1, 0.75, 0.5 …, never below `min_scale`) and climbs back when there is room. A single slow frame never triggers it; a sustained overload does. * **Errors are isolated.** `on_error="skip"` (the default) counts an exception in your function and keeps going; `"raise"` stops the pipeline. * **No allocation per frame.** Sources fill a small ring of reused buffers; the frame helpers write into reused arrays. ### Cameras `rt.camera(index, size, fps=…)` opens a camera through OpenCV with the settings that matter for latency: a one-frame driver buffer, MJPG at 1280×720 and above (USB cameras reach 60–120 fps there only in MJPG), V4L2 on Linux and DirectShow on Windows. It needs `--allow-python` (OpenCV is a Python library) and `pip install opencv-python`. ### CPU or GPU ```apsi rt.backend("cpu") # default: OpenCV (SIMD, multithreaded) when installed, otherwise NumPy rt.backend("gpu") # frame helpers on CUDA through PyTorch or CuPy; without a usable card it stays on the CPU rt.backend("auto") # GPU only for frames large enough to beat the copy to the card (≥ 1600×900) ``` Frame helpers: `gray`, `resize`, `downsample`, `diff`, `motion`, `ema`, `box_blur`, `threshold`, `edges`, `fps_meter`. ### Your own per-pixel kernels Anything the helpers do not cover, write as a `@gpu` function: one GPU thread per pixel, and on a machine without an NVIDIA GPU the same function runs as native code on all CPU cores. Keep output buffers in GPU memory with `to_gpu` so frames do not travel back and forth (`examples/43_gpu_video.apsi`): ```apsi @gpu fn brightness_mask(frame, out, level: Float): parallel for y in 0..frame.shape[0]: # rows x columns: one thread per pixel for x in 0..frame.shape[1]: let v = 0.299 * frame[y, x, 0] + 0.587 * frame[y, x, 1] + 0.114 * frame[y, x, 2] out[y, x] = 1.0 if v > level else 0.0 let out = to_gpu(zeros(1080, 1920, dtype=f32)) rt.video(rt.camera(0, "fhd"), fn(frame, info) => brightness_mask(frame, out, 128.0), fps=60) ``` # Tables (`use dataframe`) A columnar table engine built into Apsi: typed columns, whole-column kernels, no per-cell objects. It reads CSV, JSON and JSON Lines, filters, groups, joins, sorts and writes, with an API close to Polars. ```apsi use dataframe as df let sales = df.read_csv("sales.csv") # needs --allow-read for the file let eu = sales.filter((df.col("amount") > 90) & (df.col("region") == "EU")) let by = sales.group_by("customer").agg(total=df.col("amount").sum(), n=df.count(), avg=df.col("amount").mean()) let top = by.sort("total", descending=true).head(3) let full = top.join(df.read_csv("customers.csv"), on="customer", how="left") print(full) full.write_csv("top.csv") # needs --allow-write ``` ```text shape: (3, 5) ┌──────────┬───────┬─────┬────────┬──────┐ │ customer │ total │ n │ avg │ tier │ │ str │ float │ int │ float │ str │ ├──────────┼───────┼─────┼────────┼──────┤ │ cid │ 300 │ 1 │ 300 │ gold │ │ ann │ 219.5 │ 2 │ 109.75 │ gold │ │ dan │ 150 │ 1 │ 150 │ null │ └──────────┴───────┴─────┴────────┴──────┘ ``` ## Building tables | | | |---|---| | `df.read_csv(path, sep=",", columns=[…])` | typed columns (int, float, bool, date, str); a C parser is used when a C compiler is present | | `df.read_jsonl(path)`, `df.read_json(path)` | JSON Lines / a JSON array of objects | | `df.from_records([{…}, …])`, `df.frame({"a": [1, 2], "b": ["x", "y"]})` | from Apsi values | | `df.from_csv_text(text)` | CSV already in memory | | `df.concat(a, b, …)` | rows of several tables | ## Expressions `df.col("name")` refers to a column; `df.lit(v)` is a constant; `df.count()` counts rows in a group. Expressions combine with `+ - * / % ** == != < <= > >= & | ~` and are evaluated column-wise, without Python loops. * numbers: `abs sqrt log exp round(n) cast("float")` * aggregates: `sum mean min max std var median count first last n_unique` * windows: `cumsum shift(k) rolling_mean(w)` * nulls and sets: `is_null fill_null(v) is_in([…])` * strings: `str_lower str_upper str_strip str_len str_contains str_startswith str_endswith str_replace(a, b)` * names: `alias("new")`, or keyword arguments: `with_columns(double=df.col("x") * 2)` * anything else: `map(fn)` (runs per value — slower) ## Table operations `select`, `with_columns`, `drop`, `rename`, `filter`, `head`, `tail`, `sample(n, seed=)`, `sort(by, descending=)`, `unique(subset=)`, `group_by(…).agg(…)`, `join(other, on= | left_on= right_on=, how="inner" | "left" | "outer", suffix="_right")`, `value_counts(column)`, `describe()`, `concat`. Reading back: `height`, `width`, `shape`, `columns`, `dtypes`, `row(i)`, `rows()`, `to_dicts()`, `to_dict()`, `to_numpy()`, `to_pandas()`, `memory_bytes()`; writing: `write_csv`, `write_jsonl`, `write_json`, `to_json`. ## How it is fast * **Columns are contiguous typed arrays** (int64, float64, bool, datetime64). A million numbers take 8 MB. * **Strings are dictionary-encoded**: an int32 code per row into a list of unique values. Comparing, grouping and joining on strings compares integers. * **Group-by** factorises the keys once and reduces with `bincount` / `reduceat`. **Joins** match the factorised keys by direct addressing. **Filters** are boolean masks. Measured against pandas 2 on 2 million rows (AMD EPYC 7763 GitHub Actions runner (4 vCPU), best of 3, results checked equal): | operation | pandas | Apsi | | |---|---|---|---| | filter with two conditions | 98.8 ms | 4.6 ms | **21.5× faster** | | group by 50,000 string keys: sum, mean, count | 202.2 ms | 35.6 ms | **5.7× faster** | | group by two keys: std, max | 170.1 ms | 39.0 ms | **4.4× faster** | | left join 2M × 50k on a string key | 168.9 ms | 45.1 ms | **3.7× faster** | | sort by a float column | 168.4 ms | 105.1 ms | **1.6× faster** | | arithmetic on columns | 2.8 ms | 2.7 ms | same | | read a 1M-row CSV | 0.18 s | 0.20 s | 1.1× slower | | memory for the table | 262 MB | 65 MB | **4× less** | Reproduce with `benchmarks/dataframe/run.py` and `benchmarks/dataframe/csv.py`. # Databases (`use db`) PostgreSQL, MySQL / MariaDB, MongoDB, Redis and SQLite clients built into Apsi's runtime — nothing to install, no driver to compile. ```apsi use db let pg = db.postgres("postgres://shop:secret@localhost:5432/shop") # needs --allow-net=localhost:5432 pg.execute("create table if not exists item (id serial primary key, name text, price numeric)") pg.query("insert into item (name, price) values ($1, $2)", "pen", 1.25) for row in pg.query("select id, name, price from item where price < $1", 10): print(row.id, row.name, row.price) # a record; row["name"] works too with pg.transaction(): # commit, or roll back on an error pg.query("update item set price = price * 1.1") ``` ## PostgreSQL * **Protocol:** the v3 wire protocol; SCRAM-SHA-256, md5 and password authentication; TLS with `sslmode=require` (or `verify-full`) in the URL. * **Parameters are never pasted into SQL.** `$1, $2 …` travel separately (the extended query protocol), so values cannot inject SQL. The server's parameter types are used, so a list goes into an `int[]` or `text[]` column. * **Prepared statements** are reused per connection (256 per connection, least recently used dropped). * **Results** become Apsi values: int, float, `Decimal` for numeric, bool, text, bytes, json/jsonb (parsed), dates and timestamps, uuid, one-dimensional arrays. | | | |---|---| | `pg.query(sql, *params)` | a `Result`: iterate rows; `.one()`, `.scalar()`, `.dicts()`, `.column(name)`, `.to_dataframe()` | | `pg.execute(sql)` | statements without results (several separated by `;`) | | `pg.insert_many(table, rows, columns=)` | bulk insert in batches | | `with pg.transaction():` | `begin` … `commit`, or `rollback` when the block raises | | `pg.listen(channel)`, `pg.notify(channel, text)`, `pg.poll(timeout)` | LISTEN / NOTIFY | | `db.pool(url, size=8)` | a thread-safe pool: `with pool.connection() as c:` | ## Redis ```apsi let r = db.redis("redis://:secret@localhost:6379/0") r.set("hits", 0) r.incr("hits") r.hset("user:1", "name", "ann") print(r.hgetall("user:1")) # {"name": "ann"} let p = r.pipeline() # many commands, one round trip for i in range(10000): p.rpush("queue", i) p.execute() let sub = r.subscribe("news") # pub/sub ``` Every command works by name (`r.zadd(…)`, `r.xadd(…)`) or as `r.command("XADD", …)`. `HGETALL` and similar replies come back as dicts. ## SQLite ```apsi let lite = db.sqlite("shop.db") # needs --allow-read / --allow-write for the file; ":memory:" needs nothing lite.execute("create table t (a, b)") lite.insert_many("t", [[1, "x"], [2, "y"]]) print(lite.query("select * from t where a > ?", 1).dicts()) ``` ## Measured Local PostgreSQL 16 and Redis 7 on a 2-core VM (`benchmarks/db/run.apsi`, best of three runs): | operation | time | |---|---| | PostgreSQL: insert 20,000 rows with `insert_many` | 191 ms | | PostgreSQL: 2,000 point queries by primary key (prepared, one round trip each) | 168 ms (84 µs each) | | PostgreSQL: read 20,000 rows into records | 46 ms | | Redis: 10,000 pipelined `RPUSH` | 29 ms | | Redis: 2,000 `SET` round trips | 97 ms (49 µs each) | ## Permissions Network databases need `--allow-net=HOST:PORT`. Passwords in URLs are not printed in errors or in `print(pg)`. Use `use env` to keep them out of the source: `db.postgres(env.secret("DATABASE_URL"))`. ## MySQL and MariaDB ```apsi let my = db.mysql("mysql://shop:secret@localhost:3306/shop") # needs --allow-net=localhost:3306 my.execute("create table if not exists item (id int auto_increment primary key, name text, price decimal(9,2))") let r = my.query("insert into item (name, price) values (?, ?)", "pen", 1.25) print(r.rowcount, r.insert_id) for row in my.query("select id, name, price from item where price < ?", 10): print(row.id, row.name, row.price) # price is a Decimal with my.transaction(): my.insert_many("item", [["ink", 3.5], ["pad", 2.0]], columns=["name", "price"]) ``` * **Authentication:** `caching_sha2_password` (the MySQL 8 default: fast path; full path over TLS or with the server's RSA key) and `mysql_native_password` (MariaDB, MySQL 5.7), including authentication switches. * **TLS:** `?ssl=true` or `?ssl-mode=REQUIRED` (`VERIFY_CA`, `VERIFY_IDENTITY` check the certificate). * **Values** are escaped by the client on a utf8mb4 connection, so a value can never end a string literal. Placeholders: `?`, `%s` or `$1`. Lists become `(1, 2, 3)` for `IN ?`. Results: int, float, `Decimal`, str, bytes (binary columns), dates, times, JSON. * `db.mariadb(…)` is the same client. `db.pool("mysql://…", size=8)` shares connections between threads. * Several statements in one `execute` run in order; the result of the last one is returned. ## MongoDB ```apsi let mongo = db.mongo("mongodb://app:secret@localhost:27017/shop?authSource=admin") # --allow-net=localhost:27017 let items = mongo.collection("items") # or mongo["shop"]["items"] let id = items.insert_one({"name": "pen", "price": 1.25, "tags": ["office"]}) items.insert_many([{"name": f"n{i}", "price": i} for i in range(1000)]) for doc in items.find({"price": {"$lt": 10}}, sort={"price": 1}, limit=20): print(doc["name"], doc["price"]) items.update_one({"name": "pen"}, {"$set": {"price": 1.5}}) print(items.count_documents({}), items.distinct("tags")) print(items.aggregate([{"$group": {"_id": none, "total": {"$sum": "$price"}}}])) items.create_index({"name": 1}, unique=true) ``` * **Protocol:** OP_MSG (MongoDB 3.6 and newer); cursors are followed with `getMore` automatically. * **Authentication:** SCRAM-SHA-256 and SCRAM-SHA-1 (`authSource`, `authMechanism`); the server's signature is verified too. TLS with `?tls=true`. `mongodb+srv://` addresses need the host written out. * **Values:** dicts keep their key order; int (32/64-bit), float, `Decimal` (decimal128), str, bytes, bool, none, datetime (UTC), `ObjectId`, regular expressions, UUIDs. * Collections: `insert_one`, `insert_many`, `find`, `find_one`, `update_one`, `update_many`, `replace_one`, `delete_one`, `delete_many`, `count_documents`, `aggregate`, `distinct`, `create_index`, `drop`; any other command with `mongo.command({...})`. ## How the clients are tested PostgreSQL 16 and Redis 7 are tested against real servers. The MySQL and MongoDB clients are tested against in-process servers that speak the real wire protocols (handshakes, both MySQL authentication methods and an authentication switch, SCRAM-SHA-256 checked on the server side, cursors with several batches, errors), and SCRAM against the RFC 7677 test vector. `APSI_TEST_MYSQL=mysql://…` and `APSI_TEST_MONGO=mongodb://…` run the same tests against your own servers. # Science, engineering and medicine Apsi ships the numerical tools of a research lab as standard modules. They need nothing beyond NumPy, accept plain numbers, tensors and physical quantities, and are checked in the test suite against SciPy, statsmodels, lifelines, scikit-learn, pydicom and exact analytic solutions (`tests/test_science.py`). | Module | What it covers | |---|---| | [`use special`](#special-functions) | gamma, beta, error functions, incomplete gamma/beta and their inverses, Bessel J/I, elliptic integrals, orthogonal polynomials, spherical harmonics, ζ, E1 | | [`use stats`](#statistics) | 11 distributions, descriptive statistics, 19 hypothesis tests, OLS and logistic regression, power and sample size, bootstrap, permutation tests, multiple-testing correction | | [`use signal`](#signal-processing) | Butterworth filters (zero-phase), notch, windows, convolution, peak detection, Welch spectra, spectrograms, Savitzky–Golay, resampling, Hilbert envelope | | [`use mechanics`](#mechanics) | symplectic integrators, Hamiltonian and Lagrangian dynamics derived by autodiff, N-body gravity, orbits, rigid bodies, normal modes, beams, trusses | | [`use pde`](#partial-differential-equations) | heat (1-D Crank–Nicolson, 2-D ADI), wave, advection, Poisson (fast transforms), 2-D Navier–Stokes, CG and BiCGSTAB | | [`use quantum`](#quantum-mechanics) | Schrödinger bound states and time evolution, operators, spins, qubit circuits, density matrices, entanglement, hydrogen | | [`use physics`](#physics-formulas) | constants and fields, plus radiation, relativity, optics, electromagnetism, thermodynamics and decay formulas with units | | [`use medical`](#medical-and-health-research) | pharmacokinetics and NCA, epidemic models, diagnostic accuracy and ROC, survival analysis (Kaplan–Meier, log-rank, Cox), clinical formulas, ECG and HRV, DICOM images | | [`use lab`](#reproducible-research) | seeds, provenance, experiment logs, result formatting, Markdown/LaTeX/CSV tables | The existing `use sci` (FFT, integration, roots, optimisation, curve fitting with uncertainties, stiff ODEs), `use linalg`, units, measurement uncertainty and automatic differentiation work together with all of them. ## Special functions ```apsi use special print(gamma(4.5), lgamma(200.0), beta(2.0, 3.0), factorial(25), comb(60, 30)) print(erf(1.0), erfinv(0.5), erfcinv(1e-300)) print(gammainc(2.5, 1.0), betainc(2.0, 3.0, 0.4), gammaincinv(2.5, 0.9)) print(besselj(0, 2.404825557695773), besseli(1, 3.0), ellipk(0.5), ellipe(0.5)) print(legendre(5, 0.3), hermite(4, 1.2), laguerre(3, 0.5, 1.0), sph_harm(2, 1, 0.7, 1.1)) ``` Every function works element-wise on tensors. Accuracy is close to double precision (relative errors of 1e-16 to 1e-13 against SciPy). `gamma`, `lgamma`, `erf`, `erfc`, `erfinv`, `factorial`, `comb`, `beta`, `besselj` and `legendre` are also in `use math`; `erf` is a differentiable built-in. ## Statistics ```apsi use stats let a = randn(40) + 0.5 let b = randn(35) let t = ttest_ind(a, b) # Welch by default print(t.statistic, t.p, t.df, t.ci, t.cohen_d) let d = student_t(12) # pdf, cdf, sf, ppf, sample, mean, var, interval print(d.ppf(0.975), d.interval(0.95)) let fit = ols(X, y, names=["dose", "age"]) # coefficients, SE, t, p, CI, R², adjusted R², F, AIC print(fit.coef.dose, fit.p, fit.r2) let lr = logit(X, outcome) # odds ratios with CI print(lr.odds_ratio, lr.odds_ratio_ci) print(sample_size_ttest(0.5, power=0.8).n) # 64 per group, exact noncentral t print(adjust_pvalues([0.01, 0.04, 0.03, 0.2], "bh")) ``` | Area | Functions | |---|---| | distributions | `normal`, `student_t`, `chi2`, `f_dist`, `gamma_dist`, `beta_dist`, `exponential`, `uniform`, `lognormal`, `binomial`, `poisson` | | descriptive | `describe` (n, mean, SD, SEM, quartiles, skewness, excess kurtosis, 95 % CI), `quantile`, `zscore`, `sem`, `mean_ci`, `corr_matrix`, `cov_matrix` | | tests | `ttest_1samp`, `ttest_ind`, `ttest_rel`, `anova_oneway` (η², ω²), `chi2_contingency` (Cramér's V), `chi2_gof`, `fisher_exact`, `mannwhitneyu`, `wilcoxon`, `kruskal`, `ks_2samp` (exact for small samples), `jarque_bera`, `pearsonr` (Fisher-z CI), `spearmanr`, `kendalltau` (τ-b), `binom_test`, `proportion_ci` (Wilson, Clopper–Pearson) | | models | `ols`, `logit` (each with `predict`) | | design | `cohen_d`, `hedges_g`, `power_ttest`, `sample_size_ttest` | | resampling | `bootstrap_ci`, `permutation_test` | | corrections | `adjust_pvalues(p, "bonferroni" \| "holm" \| "bh" \| "by")` | Every test takes `alternative="two-sided" | "less" | "greater"` where it applies, and returns a record with the statistic, the p-value and what a paper reports (degrees of freedom, confidence intervals, effect sizes). The results agree with SciPy and statsmodels to 1e-10 or better. ## Signal processing ```apsi use signal let sos = butter(4, [0.5, 40.0], "bandpass", 500.0) # second-order sections let clean = filtfilt(sos, ecg) # zero phase, SciPy-compatible edges let y = notch(clean, 50.0, 500.0) # mains hum let pk = find_peaks(y, height=0.5, distance=100, prominence=0.3) let psd = welch(y, 500.0, nperseg=1024) # record(f, psd) let sg = spectrogram(y, 500.0, 256, 128) # record(f, t, power) ``` Also: `sosfilt`, `freqz`, `lowpass`, `highpass`, `bandpass`, `bandstop`, `hann`, `hamming`, `blackman`, `kaiser`, `convolve`, `correlate`, `savgol`, `moving_average`, `detrend`, `resample`, `hilbert`, `envelope`. Filter design, filtering, peaks, Welch spectra, Savitzky–Golay, resampling and the Hilbert transform agree with `scipy.signal` to 1e-8 or better. When SciPy is installed its compiled filter loop is used for speed; otherwise a built-in loop gives the same result. ## Mechanics ```apsi use mechanics # any force law: x'' = accel(x, v, t) fn spring(x) = -x let r = simulate(spring, tensor([1.0]), tensor([0.0]), 0.01, 10000, "yoshida4") # Lagrangian mechanics: the equations of motion are derived by automatic differentiation let g = 9.81 fn L(q, qd) = qd[0]^2 + 0.5*qd[1]^2 + qd[0]*qd[1]*cos(q[0]-q[1]) + 2*g*cos(q[0]) + g*cos(q[1]) let pend = lagrangian_flow(L, tensor([1.5, -0.5]), tensor([0.0, 0.0]), 0.001, 5000) print(max(pend.energy) - min(pend.energy)) # energy is conserved to ~1e-8 # gravity let solar = nbody(masses, positions, velocities, 3600.0, 24 * 365, softening=1e3) print(solar.energy_drift) ``` | Area | Functions | |---|---| | integration | `simulate` (Verlet, 4th-order Yoshida, RK4), `hamiltonian_flow(H, q0, p0, …)`, `lagrangian_flow(L, q0, qdot0, …)` | | gravity and orbits | `nbody`, `kepler_solve`, `orbit_elements`, `orbit_state`, `orbital_period`, `vis_viva`, `hohmann` | | rigid bodies | `inertia_tensor`, `principal_axes`, `rotation_matrix`, `quat`, `quat_mul`, `quat_conj`, `quat_rotate`, `quat_to_matrix`, `rigid_body` (Euler's equations with torques) | | vibrations | `normal_modes(K, M)` | | structures | `beam` (Euler–Bernoulli finite elements: cantilever, simply supported, fixed, propped; point and distributed loads; deflection, slope, moment, shear, reactions), `truss2d` (direct stiffness: displacements, member forces, reactions) | ## Partial differential equations ```apsi use pde let r = heat1d(u0, dx, dt, 1000, alpha, bc="neumann", every=100) # record(t, x, u[frame, i]) let w = wave1d(u0, 0.0, c, dx, dt, 2000, bc="fixed") let phi = poisson2d(rho / epsilon, dx) # exact solution of the 5-point scheme let flow = navier_stokes2d(omega0, nu=1e-3, dt=1e-3, steps=5000, every=500) let s = cg(A, b) # A: matrix or function x -> A·x ``` `heat2d` (ADI), `advection1d` (upwind, Lax–Wendroff), `bicgstab` and `tridiag_solve` (batched Thomas) are also available. The tests compare every solver with exact solutions: decaying sine modes, standing waves, the discrete Laplacian and the Taylor–Green vortex (to 1e-15). ## Quantum mechanics ```apsi use quantum let x = linspace(-10.0, 10.0, 2001) let s = schrodinger1d(fn(x) => 0.5 * x * x, x, mass=1.0, n=5, hbar=1.0) # 0.5, 1.5, 2.5, … (1e-8) let wave = evolve1d(gaussian_packet(x, -3.0, 0.5, 4.0), fn(x) => 0.0, x, 0.001, 2000, mass=1.0, hbar=1.0) var psi = ket("00") psi = apply(psi, gates.H, 0) psi = apply(psi, gates.CNOT, [0, 1]) print(measure(psi, shots=1000, seed=1), concurrence(psi), entropy(partial_trace(density(psi), [0]))) ``` SI units are the default (`mass = m_e`, `hbar = ħ`); pass `hbar=1.0, mass=1.0` for natural units. Bound states use fourth-order finite differences with ψ = 0 beyond the ends of the grid. Also: `expect_x`, `expect_p`, `uncertainty`, `probability`, `dagger`, `commutator`, `anticommutator`, `kron`, `expm`, `expm_hermitian`, `time_evolution`, `pauli`, `spin_ops(s)`, `rx`, `ry`, `rz`, `phase`, `controlled`, `probabilities`, `purity`, `fidelity`, `bloch`, `ghz`, `bell`, `hydrogen_energy`, `hydrogen_radial`. ## Physics formulas `use physics` now also contains formulas that take and return quantities with units: ```apsi use physics print(wien_peak(5778K)) # 5.0e-07 m print(planck(500e-9[m], 5778K)) # spectral radiance print(lorentz_factor(0.9 * c), time_dilation(1[s], 0.9 * c)) print(coulomb_field([1e-9], [[0.0, 0.0, 0.0]], [[1.0, 0.0, 0.0]])) print(biot_savart(loop_points, 2.0, [[0.0, 0.0, 0.0]])) ``` Thermal radiation (`planck`, `wien_peak`, `blackbody_power`), relativity (`lorentz_factor`, `relativistic_energy`, `relativistic_momentum`, `lorentz_boost`, `time_dilation`, `length_contraction`, `velocity_add`), waves and optics (`doppler`, `relativistic_doppler`, `snell`, `critical_angle`, `brewster_angle`, `fresnel`, `thin_lens`, `single_slit`, `double_slit`, `photon_energy`, `de_broglie`), electromagnetism (`coulomb_field`, `coulomb_potential`, `biot_savart` (exact finite segments), `lorentz_force`, `cyclotron_frequency`, `skin_depth`), thermodynamics (`ideal_gas_pressure`, `carnot_efficiency`, `maxwell_speed_pdf`, `mean_speed`, `rms_speed`, `heat_conduction`) and decay (`decay`, `activity`, `half_life_from_rate`). ## Medical and health research > These functions are for research and education. They are not a medical device and must not be used on their > own to make decisions about the care of a patient. The text is also available in programs as `DISCLAIMER`. ```apsi use medical # pharmacokinetics let c = pk_one_compartment(500, 40, times, CL=5.0, route="oral", ka=1.1, tau=12, doses=6) let r = nca(times, measured, dose=500) # Cmax, Tmax, AUC (linear-up/log-down), λz, t½, CL/F, Vz/F, MRT # survival analysis let km = kaplan_meier(time, event) # survival, Greenwood SE, log-log CI, median let lr = logrank(time, event, group) let cox = cox_ph(time, event, X, names=["age", "treatment"]) print(cox.hazard_ratio, cox.hr_ci, cox.p, cox.concordance) # diagnostic accuracy print(diagnostic_metrics(90, 20, 10, 180)) # sensitivity, specificity, PPV, NPV, LR±, DOR with CIs let roc_ = roc(scores, labels) # AUC with DeLong CI, Youden threshold print(roc_compare(model_a, model_b, labels).p) # signals and images let beats = ecg_rpeaks(ecg, 360.0) print(hrv(beats.rr)) # SDNN, RMSSD, pNN50, LF, HF, LF/HF let ct = read_dicom("slice.dcm") # pixels in Hounsfield units, spacing in mm let view = window_level(ct.pixels, 40, 400) # soft-tissue window ``` | Area | Functions | |---|---| | pharmacokinetics | `pk_one_compartment` (IV bolus, infusion, oral; repeated dosing), `pk_two_compartment`, `nca` | | epidemiology | `sir`, `seir`, `final_size`, `incidence_rate` (exact Poisson CI), `risk_measures` (RR, OR, RD, NNT with CIs), `mantel_haenszel` (pooled OR/RR, CMH test) | | diagnostic accuracy | `diagnostic_metrics`, `roc`, `roc_compare` (paired DeLong), `cohen_kappa`, `brier_score` | | survival | `kaplan_meier`, `logrank` (any number of groups), `cox_ph` (Efron ties), `concordance_index` | | clinical formulas | `bmi`, `bsa` (Mosteller, DuBois), `egfr_ckd_epi_2021` (race-free), `cockcroft_gault`, `qtc` (Bazett, Fridericia, Framingham, Hodges), `mean_arterial_pressure`, `ideal_body_weight`, `anion_gap` | | signals | `ecg_rpeaks` (Pan–Tompkins style), `hrv` | | images | `read_dicom`, `read_dicom_series` (3-D volume sorted along the slice normal), `window_level` | Kaplan–Meier, the log-rank test and Cox regression agree with lifelines to 1e-9 or better. ROC AUC and kappa agree with scikit-learn, and the 2×2 and Mantel–Haenszel measures with statsmodels. DICOM pixels are bit-identical to pydicom for explicit/implicit little-endian and big-endian files. `read_dicom` reads uncompressed DICOM files and leaves patient-identifying fields out unless you pass `include_identifiers=true`. Compressed transfer syntaxes (JPEG, JPEG 2000, RLE) give a clear message asking you to decompress the file first. Reading DICOM needs `--allow-read`, like any other file access. ## Reproducible research ```apsi use lab set_seed(42) # Apsi, numpy and Python's random module let run = experiment("dropout-sweep", record(dropout=0.1, lr=3e-4)) # needs --allow-write for epoch in 0..20: run.log(epoch, record(loss=loss, val_auc=auc)) run.result(record(test_auc=0.912)) run.finish() # experiments/dropout-sweep.jsonl print(format_measurement(9.8123, 0.0312, "m/s^2")) # 9.81(3) m/s^2 print(table_latex([record(model="A", auc=0.91), record(model="B", auc=0.87)], caption="Results")) let runs = load_runs("experiments") # every run with params, logs and provenance ``` Every run records its provenance: the Apsi, Python and NumPy versions, OS, CPU, GPUs, seed and time (UTC). User names, host names and paths are not recorded. Tables come as Markdown (`table_markdown`), LaTeX with booktabs and escaping (`table_latex`) or CSV (`table_csv`). `sigfig` rounds to significant figures. # Libraries (.apsil) Apsi libraries are distributed as **`.apsil`** files through the official registry. One command installs the newest version, every file is verified, and a library is used exactly like a standard module. ## Installing ```bash apsi install NAME # newest stable version apsi install NAME@1.4.2 # exactly this version apsi install NAME@^1.4 # newest 1.x that is at least 1.4 apsi install NAME@~1.4.2 # newest 1.4.x that is at least 1.4.2 apsi install "NAME>=1.2,<2" # any range apsi install ./dist/NAME-1.0.0.apsil # a local file apsi install # everything in [dependencies] of apsi.toml apsi install -U NAME # upgrade to the newest matching version apsi install -g NAME # for your user account instead of the project apsi uninstall NAME apsi lib list # installed libraries apsi search WORDS # search the registry apsi lib info NAME # versions, sizes and dates ``` Inside a project (a folder with `apsi.toml`), libraries go into `apsi_packages/` and a lock file, `apsi-packages.lock`, records the exact versions and file hashes. Outside a project, or with `-g`, they go into your user folder (`~/.apsi/packages`, or `%USERPROFILE%\.apsi\packages` on Windows). Project libraries take precedence. Dependencies of a library are installed automatically. If two libraries need incompatible versions of the same dependency, installation stops with a clear message instead of picking one silently. Pre-release versions (`2.0.0-rc.1`) are installed only when a requirement names a pre-release. ### Version requirements | Requirement | Allows | |---|---| | `*` or nothing | the newest stable version | | `1.4.2` or `==1.4.2` | exactly 1.4.2 | | `1` | any 1.x.y | | `1.4` | any 1.4.y | | `^1.4.2` | ≥ 1.4.2 and < 2.0.0 (for 0.x: `^0.3.1` allows < 0.4.0) | | `~1.4.2` | ≥ 1.4.2 and < 1.5.0 | | `>=1.2,<2` | a combination of `>`, `>=`, `<`, `<=`, `==`, `!=` | ## Using a library ```apsi use physics_units # dashes in the library name become underscores print(physics_units.version) use transformer_kit as tk let model = tk.gpt(layers=12, width=768) ``` `use NAME` makes the library’s public names available directly and as `NAME.member`. Names starting with `_` are private. The checker loads the library before your program runs, so unit and type errors across library boundaries are reported as usual. ## What is verified on installation 1. The SHA-256 of the downloaded file must match the registry’s record. 2. The archive must contain exactly the files listed in its manifest, with the listed sizes; paths may not leave the package folder; encrypted entries are refused. 3. Every file’s SHA-256 must match the manifest. 4. The library must accept your Apsi version (`apsi = ">=2.0.0"` in its manifest). 5. **Apsi Guard** scans the code. Critical findings remove the library again; high findings are shown. ## Creating a library ```bash apsi new my-physics --template lib ``` The `[package]` table of `apsi.toml` describes the library: ```toml [project] name = "my-physics" # lowercase letters, digits, "-" or "_"; 2–64 characters; starts with a letter version = "1.0.0" # semantic versioning description = "Orbital mechanics helpers" [package] entry = "src/main.apsi" # what `use my_physics` loads summary = "Orbital mechanics helpers" license = "MIT" authors = ["Your Name"] keywords = ["physics", "orbits"] homepage = "https://example.org" repository = "https://github.com/you/my-physics" apsi = ">=2.0.0" # Apsi versions the library works with include = ["src/**", "README*", "LICENSE*", "examples/**", "data/**"] exclude = ["data/raw/**"] [dependencies] linalg-extra = "^1.2" ``` Libraries may be of any size and contain data files (weights, tables, datasets) next to the code; files larger than 2 GB are stored with ZIP64. ## Building: `apsi pack` ```bash apsi pack # writes dist/my-physics-1.0.0.apsil apsi pack --out build/ # another folder ``` `apsi pack` first checks every `.apsi` file (syntax, names, types and units) and stops if anything is wrong. It then writes the archive deterministically — sorted entries, fixed timestamps — so the same sources always give the same bytes and the same SHA-256. ## Publishing: `apsi publish` Libraries are published only from the command line, with an API token: 1. On the website, open **Settings → Developer** and create a token. Give it a name, choose its permissions (`publish:new` to create new libraries, `publish:update` to add versions, `yank` to hide versions) and optionally limit it to one library. Copy it — it is shown once. 2. Store the token (the file is readable only by you). The official registry, `https://apsi.dev`, is used by default; `apsi lib registry URL` (or `APSI_REGISTRY`) switches to a private one. ```bash apsi lib login --token apsi_… ``` 3. Publish: ```bash apsi pack apsi publish # uploads the newest file in dist/ apsi publish dist/my-physics-1.0.0.apsil --token apsi_… ``` In continuous integration, set `APSI_TOKEN` (and `APSI_REGISTRY`) as secret environment variables instead of storing them. Rules enforced by the registry: - Files larger than 64 MB are uploaded automatically in resumable 32 MB pieces, so libraries of any size get through proxies that limit request sizes; an interrupted upload continues where it stopped. - A version can be published once and never replaced. Publish a new version to fix a mistake. - Only the owner can publish new versions of a library. Names that differ only in `-` and `_` are the same name. - A broken version can be **yanked**: new installations skip it, while lock files that already name it keep working. ## The .apsil format An `.apsil` file is a ZIP archive (format version 1): ```text apsil.json manifest src/… the library's source files README.md, LICENSE, examples/, data/ … ``` `apsil.json` holds `format` (`"apsil"`), `format_version`, `name`, `version`, `summary`, `authors`, `license`, `homepage`, `repository`, `keywords`, `apsi` (supported Apsi versions), `entry`, `dependencies`, `built_with`, and `files` — the path, size and SHA-256 of every other file in the archive. ## Registry API The registry speaks a small JSON API, so other tools can use it too: | Request | Answer | |---|---| | `GET /api/v1/packages/NAME` | name, summary, owner, latest, downloads, and every version with size, SHA-256, publish time and download URL | | `GET /api/v1/packages/NAME/VERSION` | one version including its manifest | | `GET /api/v1/packages/NAME/VERSION/download` | the file (supports `Range`) | | `GET /api/v1/search?q=WORDS` | matching libraries | | `POST /api/v1/upload` | multipart form with `file`, `name`, `version`, `sha256` — or JSON `{"upload_id": …}` after a chunked upload; header `Authorization: Bearer TOKEN` | | `POST /api/v1/uploads` · `PUT /api/v1/uploads/ID` · `GET /api/v1/uploads/ID` | resumable chunked upload: create with `{"purpose": "library", "size", "sha256"}`, send pieces with `Content-Range: bytes START-END/TOTAL`, ask how much arrived | | `POST /api/v1/packages/NAME/VERSION/yank` | JSON `{"yanked": true}` or `false`; needs the `yank` permission | ## Python packages `apsi pkg` (described in [Interop](/docs/2.4.4/interop)) manages *Python* packages used through `use python`. It is separate from Apsi libraries. # Publishing to apsi.dev — libraries, releases and benchmarks Everything on the website can be added and removed **from the command line on any computer**, or with the server command when you are on the server itself. ## Libraries (every member) 1. On the site: **Settings → Developer → New token**, permissions `publish:new`, `publish:update` (and `yank` if you want to remove versions later). 2. Once per computer: `apsi lib login --token apsi_…` 3. In your library folder: ```bash apsi pack # checks the sources, builds dist/NAME-VERSION.apsil apsi publish # uploads it (large files go up in resumable 32 MB pieces) apsi search physics # find libraries apsi install geom-tools # use one: then `use geom_tools` ``` Removing a version: ```bash apsi yank NAME 1.2.0 # hidden from new installs; projects that use it keep working (undo: --undo) apsi unpublish NAME 1.2.0 # deleted for good — allowed in the first 72 hours or while nobody downloaded it ``` Versions can never be replaced: publish `1.2.1` instead. That guarantee is what keeps everyone's builds reproducible. ## Apsi releases (administrators) Who is an administrator is decided only on the server: signing up never makes one, not even the first account. The owner runs `node scripts/admin.js user promote NAME` (Windows package: `admin user promote NAME`) on the server. The `release` permission is offered only to administrators who have turned on two-factor sign-in, and a release token stops working if two-factor sign-in is turned off or the account is demoted. Tokens are stored as hashes, can expire, and can be revoked in *Settings → Developer*. Create a token with the `release` permission, store it with `apsi lib login`, then: ```bash apsi release upload ApsiSetup-2.4.4-x64.exe apsi-2.4.4-linux.zip apsi-2.4.4-source.zip --version 2.4.4 apsi release upload apsi-1.10.0-linux.zip --version 1.10.0 --date 2026-11-08T09:00:00Z # an older version apsi release list # ids, sizes, downloads apsi release hide 7 # / unhide 7 apsi release delete 7 # removes the file for good ``` The platform is recognised from the file name (`.exe` → Windows, `linux`/`.deb`/`.sh` → Linux, `source` → source code); pass `--platform` to override and `--title`/`--notes NOTES.md` for the download page. The newest stable version is what `/download` shows; every version stays on `/download/all`. On the server itself the same works without a token: ```bash node scripts/admin.js release add FILE... --version 2.4.4 node scripts/admin.js release list node scripts/admin.js release delete 7 8 9 node scripts/admin.js release delete-version 1.10.0 # every file of a version ``` ## Benchmarks (administrators) ```bash python3 benchmarks/heavy/run.py --publish 2.4.4 # numbers appear on /benchmarks live python3 benchmarks/heavy/run.py --apsi-root ../apsi-1.10.0 --only Apsi --label "Apsi 1.10.0" \ --merge benchmarks/heavy/results.json --publish 1.10.0 # measure an older version apsi release bench results.json --version 2.4.4 # publish an existing results file apsi release bench --delete 1.10.0 # remove a version's results ``` The benchmarks page shows the newest version first and lets visitors pick any other version and compare two versions side by side. # Using Apsi with other languages Apsi keeps a clear border between its own world and the outside: values that cross it are converted by fixed rules (quantities with units always travel as `{"value": ..., "unit": "..."}`), and every cross-language call needs a permission first. ## Python ```apsi # apsi run app.apsi --allow-python let np = pyimport("numpy") let scipy_special = pyimport("scipy.special") print(scipy_special.gamma(5.5)) ``` - Python objects behave like normal values: call methods and read attributes — except attributes that start with `_`, which are blocked for safety - `to_py(x)` converts an Apsi value into a Python/JSON value; `from_py(v)` converts back ### Calling Apsi from Python ```python import apsi it = apsi.Interpreter(permissions={"fs"}) env = it.run(open("physics.apsi", encoding="utf-8").read(), "physics.apsi") kinetic = env.vars["kinetic"] # Apsi functions can be called from Python directly ``` ## PyTorch ```apsi # apsi run train.apsi --allow-python let t = to_torch(randn(4, 4), device="cuda") # Apsi tensor -> torch.Tensor let back = from_torch(t @ t) # torch.Tensor -> Apsi tensor # whole models: `use layers` then torch_module(model, params) -> torch.nn.Module (see BACKENDS.md) ``` ## C / C++ / Rust (FFI) ```c // vecmath.c -> gcc -O3 -shared -fPIC vecmath.c -o libvecmath.so double dot(const double *a, const double *b, int64_t n); ``` ```apsi # apsi run app.apsi --allow-ffi let lib = cload("libvecmath.so") let c_dot = lib.fn("dot", [ptr_f64, ptr_f64, i64], f64) print(c_dot(a, b, len(a))) ``` - Types: `f64 f32 i32 i64 void cstr ptr_f64 ptr_f32 ptr_i32 ptr_i64` - Tensors passed as `ptr_*` are **not copied**, so C code can write its results straight into an Apsi tensor - C++: wrap the functions in `extern "C" { ... }` - Rust: `#[no_mangle] pub extern "C" fn ...`, built as a `cdylib` - Quantities must be passed as `x.magnitude`, so you choose the unit on purpose - `library_path("vecmath")` shows the file `cload` would open (`vecmath.dll` on Windows, `libvecmath.so` on Linux); `--allow-ffi=vecmath.so@` pins a library to its hash ## JavaScript / Next.js / Java / Go / C# (HTTP) ```bash apsi serve model.apsi --port 8787 --only predict kinetic ``` - `GET /functions` lists the functions with their parameter types - `POST /call/` with the body `{"args": [...], "kwargs": {...}}` - arguments are checked for type, unit and shape on every call; a mismatch returns HTTP 400 with `{"error", "kind"}` - CORS is enabled and the body size is limited to 10 MB **Next.js (App Router, server action or route handler):** ```ts export async function kinetic(massKg: number, speed: number) { const r = await fetch("http://127.0.0.1:8787/call/kinetic", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ args: [{ value: massKg, unit: "kg" }, { value: speed, unit: "m/s" }] }), }); const { result, error } = await r.json(); if (error) throw new Error(error); return result; // { value: 9, unit: "J" } } ``` **Java 11+:** ```java var client = java.net.http.HttpClient.newHttpClient(); var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create("http://127.0.0.1:8787/call/predict")) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString("{\"args\": [[1, 2, 3]]}")) .build(); System.out.println(client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString()).body()); ``` For a full server with routes, middleware and authentication use `use web` — see [`SERVER.md`](/docs/2.4.4/server). ## Files and network - `load_csv`, `save_csv`, `load_json`, `save_json`, `read_text`, `write_text` need the `fs` permission; relative paths are resolved against the `.apsi` file that is running - `http_get(url)` and `http_post_json(url, data)` need the `net` permission # Writing servers with Apsi (`use web`) ```apsi use web let app = server(name="my-api") @app.get("/hello/{name}") fn hello(req) = {"message": "Hello " + req.params["name"]} @app.post("/energy") fn energy(req): let b = req.json() # {"mass": {"value": 2, "unit": "kg"}, ...} return {"energy": 0.5 * b["mass"] * b["speed"] ** 2} # units are checked on every request app.run(host="0.0.0.0", port=8080) # run with --allow-net ``` ## Architecture | Part | How it works | |---|---| | Accepting connections | one asyncio event loop holds tens of thousands of keep-alive clients with little memory | | Processing | handlers run on a worker thread pool (`workers=`), so numpy/AI work runs truly in parallel and never blocks accepting requests. A route that proves fast (under 0.5 ms, measured by CPU time so a busy server does not mislead it) runs directly on the event loop, without a thread hand-off; a route that is slow or blocks on I/O stays on the workers | | Several cores | `app.run(processes="auto")` (one per CPU) or `processes=N` forks processes on the same port (SO_REUSEPORT, Linux and macOS) and the kernel spreads connections between them. Each process has its own memory: keep shared state in a database or `memory.store()` | | Event loop | uvloop (libuv) when it is installed — `pip install "apsi[web]"` or `pip install uvloop` — otherwise asyncio's loop; `APSI_UVLOOP=0` turns uvloop off. The start-up line names the loop in use | | Per-request cost | request ids from a counter (no system call), the security headers merged in one step, routes without parameters found by a dictionary lookup, the header block validated by one regular expression, one timer per connection instead of one per request, and fast routes timed by the wall clock only | | Shutdown | on SIGINT/SIGTERM the server stops accepting new connections and lets running requests finish (graceful shutdown) | **Measured — a synthetic benchmark** (a route that returns a fixed 25-byte JSON object, keep-alive, the Go load generator in `benchmarks/web` on the same machine): | AMD EPYC 7763 GitHub Actions runner (4 vCPU), best of 3 rounds | 64 connections | 256 connections | |---|---:|---:| | Apsi, 1 process | 14,008–14,978 req/s | 13,671–14,830 req/s | | Apsi, 1 process, uvloop | 15,493–16,863 req/s | 14,958–15,392 req/s | | Apsi, `processes="auto"` | **41,718–42,085 req/s** | **44,042–44,269 req/s** | | Node.js 22 bare `http` (1 process) | 19,872–22,198 req/s | 22,862–25,155 req/s | Per process, the work per request went down by about 16 % in this release (measured on a 2-core VM). With one process per core Apsi answers about 1.8× the requests of a single Node.js process; one Apsi process answers about two thirds of Node's (Node.js itself needs its `cluster` module to use more than one core). With 1,000 open connections no request fails (15,454 req/s); while 64 connections keep a blocking 10 ms route busy, a fast route on the same process still answers 9,888 requests/s with a p99 of 7.9 ms (the slow route runs on worker threads, the fast one on the event loop). **What these numbers are and are not.** This is a synthetic test of the server's fixed cost per request: no database, no request body to parse, no TLS, no real network (loopback), and the load generator shares the cores with the server. It shows how much work the server itself adds. It does not predict a real application, where a request usually spends milliseconds in the database, in other services or in the handler's own work; there, the per-request overhead measured here (tens of microseconds) is a small part of the total, and the worker threads (which run handlers in parallel) and `processes="auto"` matter more. Measure your own routes with `tools/loadtest.py` or `benchmarks/web/`. ```bash python tools/loadtest.py http://127.0.0.1:8080/health --requests 20000 --concurrency 500 ``` ## Security (on by default) | Threat | Protection | |---|---| | Slowloris (slow headers) | `header_timeout` (10 s), `body_timeout` (30 s), `keepalive_timeout` (15 s) | | Oversized body/headers | `max_body` (1 MB) → 413; headers over 16 KB or over 100 lines → 431 | | Request smuggling | duplicate `Content-Length`, chunked requests and obs-fold headers are rejected | | Header injection | headers containing line breaks are dropped | | Brute force / DoS | `app.rate_limit(rate, per, burst)` — a token bucket per IP → 429 with `Retry-After`; connection limit (`max_connections`) | | Stuck handlers | `handler_timeout` → 504 | | Path traversal | `app.static()` checks the real path, so nothing outside the folder can be served (including `%2e%2e`) | | Information leaks | clients never see a stack trace (only a `request id`); details go to the server log | | Clickjacking / MIME sniffing | `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, CSP, `Referrer-Policy`, `Permissions-Policy`, HSTS with TLS | | Authentication | `jwt_sign` / `jwt_verify` (HS256, `alg=none` refused, `exp`/`nbf` checked), `require_jwt`, `require_api_key` (constant-time compare) | | Passwords | `use crypto` → `password_hash` (scrypt, or PBKDF2 with 600k rounds) | | TLS | `app.run(tls_cert="cert.pem", tls_key="key.pem")` — TLS 1.2 or newer | | Program permissions | opening a port needs `--allow-net`; serving files needs the `fs` permission | Socket-level tests for all of this live in `tests/test_web.py`. ## API ```apsi let app = server(name="api", max_body=1048576, workers=64, max_connections=10000, log=true, handler_timeout=60[s], header_timeout=10[s], body_timeout=30[s], keepalive_timeout=15[s], security_headers=true) app.get(path, handler) / @app.get(path) # also post, put, patch, delete, route(method, path) app.before(fn(req) => none | response) # middleware before the handler app.after(fn(req, resp) => ...) # middleware after the handler app.rate_limit(100, per=1[s], burst=200) app.cors(["https://example.com"]) # or "*" app.static("/assets", "public") app.health() # GET /health app.expose_metrics() # GET /metrics: requests, errors, status codes, p50/p90/p99, req/s app.stats() # the same numbers inside your program app.handle("GET", "/path", body, headers) # call a route directly without the network (for tests) app.run(host, port, tls_cert, tls_key, processes, workers) app.run_background(port=0) # run on a separate thread; returns record(port, url, stop) ``` **Path patterns:** `/users/{id:int}`, `/files/{rest:path}`, `/x/{v:float}`, `/name/{s}` **Request:** `req.method`, `req.path`, `req.params`, `req.query`, `req.headers`, `req.header(name)`, `req.body` (bytes), `req.text`, `req.json()`, `req.client`, `req.id`, `req.user` (set by `require_jwt`) **Handler return values:** dict / list / tensor / quantity → JSON; `Str` → text; `response(body, status, headers, type)`; `redirect(url)`; `abort(status, message)` to stop with an error; `none` → 204 **Errors:** a UnitError/ShapeError/TypeError caused by the client's data → 400; any other error → 500 ## WebSocket ```apsi use web let app = web.server() @app.websocket("/echo") fn echo(ws, msg): # message mode: called once per message; no thread waits per connection, ws.send(msg) # so tens of thousands of idle connections cost almost nothing @app.websocket("/chat", origins=["https://example.com"], max_message=1_000_000, ping=20) fn chat(ws): # session mode: one conversation per connection ws.join("lobby") for msg in ws: # ends when the client closes app.room("lobby").send({"from": ws.id, "text": msg}) # dicts and lists are sent as JSON app.run(port=8080) ``` * `app.websocket(path, handler, origins=, max_message=, ping=, on_open=, on_close=, subprotocols=)` — the handler takes `(ws, msg)` for message mode or `(ws)` for session mode. * `ws.send(text | bytes | dict | list)`, `ws.receive(timeout=)`, `ws.close(code, reason)`, `ws.join(room)`, `ws.leave(room)`, `ws.receive_json()`, `ws.id`, `ws.request` (the upgrade request: path, headers, query). * `app.room(name).send(msg, exclude=ws)` broadcasts to every member; `len(app.room(name))` counts them. * Messages of one connection are handled in order; slow receivers get back-pressure instead of unbounded buffers. * RFC 6455 in full: text and binary, fragmentation, ping/pong keep-alive (dead peers are dropped), the closing handshake, UTF-8 checking of text, client masking enforced, a size limit per message (16 MB by default) and Origin checking. Client (needs `--allow-net` for the host): ```apsi let c = web.websocket_connect("ws://localhost:8080/echo") c.send("hi") print(c.receive()) c.close() ``` Measured on an AMD EPYC 7763 GitHub Actions runner (4 vCPU) with a Node.js 22 client: a round trip takes 225 µs; a burst of 20,000 messages is echoed at 50,591 messages/s, in order; 1,000 concurrent connections all answered. Reproduce with `benchmarks/websocket/`. ## Talking to Next.js / JavaScript / Java See [`INTEROP.md`](/docs/2.4.4/interop). Values with units always travel as `{"value": 9.0, "unit": "J"}`. # Configuration, secrets and virtual environments ## `.env` files (`use env`) ```apsi use env env.load(".env") # KEY=value lines let db_url = env.get("DATABASE_URL") # the .env file first, then the real environment let key = env.secret("OPENAI_API_KEY") # prints as ******; key.reveal() gives the text let port = env.int("PORT", 8080) let debug = env.bool("DEBUG", false) # true/false, 1/0, yes/no, on/off let hosts = env.list("ALLOWED_HOSTS") # comma-separated env.require("DATABASE_URL", "SECRET_KEY") # one clear error listing everything that is missing ``` The file format is the one python-dotenv and Docker Compose read: `KEY=value`, `export KEY=value`, single and double quotes, multi-line double-quoted values, `\n` escapes in double quotes, `# comments`, and `${OTHER}` expansion. `apsi run app.apsi --env-file .env` loads the file before the program starts. **Permissions:** values from a `.env` file need only `--allow-read` for that file. Reading the process environment needs `--allow-env` (or `--allow-env=PORT,DEBUG` for named variables). A default (`env.int("PORT", 8080)`) is used when the permission is missing, so a program still starts. **Secrets stay secret:** a `Secret` prints and logs as `******`, is left out of error messages, and compares in constant time. Pass it to `db.postgres(...)`, `web` clients or `crypto` functions directly; call `.reveal()` only where the text itself is needed. ## Python virtual environments ```bash apsi run app.apsi --venv .venv # pyimport() uses the packages installed in that venv apsi run app.apsi # a .venv or venv folder next to the program is found automatically ``` The venv's `site-packages` are put first on the import path for `pyimport`/`pyfrom` and converted programs. The venv's Python version must match the one Apsi runs on (Apsi says so when it does not). ## Reference (`use env`) | Function | Returns | Behaviour | |---|---|---| | `env.load(path=".env", override=true, export=false)` | names defined | Reads a `.env` file. A missing file is an error. With `override=false`, names already loaded keep their value. With `export=true`, the values are also put into the process environment for child processes. Needs `--allow-read` for the file. | | `env.get(name, default=none)` | `Str` or default | The loaded `.env` value first, then the process environment. Without a permission to read the environment, returns `default` when one is given and raises a permission error otherwise. | | `env.has(name)` | `Bool` | Whether the name is set in the loaded files or the environment. | | `env.require(name, …)` | the values | Raises one error listing **every** missing or empty name, so a deployment shows all problems at once. | | `env.secret(name, default=none)` | `Secret` or `none` | The value wrapped so it never prints. `.reveal()` returns the text. | | `env.int(name, default=none)`, `env.float(…)` | number | Converts the value. An empty or missing value gives `default`. A value that is not a number is an error (never silently 0). | | `env.bool(name, default=false)` | `Bool` | `1`, `true`, `yes`, `on`, `y` (any case) are true; any other set value is false. | | `env.list(name, sep=",", default=none)` | `List[Str]` | Splits and trims; empty items are dropped. | | `env.parse(text)` | `Dict` | Parses `.env` text without touching any state — useful in tests. | | `env.values` | `Dict` | Everything loaded from files so far. | ### File format in detail ```dotenv # comments start with # PORT=8080 export DEBUG=true # "export " is allowed and ignored NAME='single quotes: taken literally, no ${EXPANSION}' GREETING="double quotes: \n becomes a line break" KEY="-----BEGIN KEY----- spans several lines -----END KEY-----" DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/app # ${NAME} from this file or the environment EMPTY= TRAILING=value # an unquoted value ends before " #" ``` A byte-order mark at the start of the file (common with Windows editors) is ignored. ### Secrets in practice ```apsi use env use db env.load() let pg = db.postgres(host=env.get("DB_HOST"), user="app", password=env.secret("DB_PASSWORD")) print(env.secret("DB_PASSWORD")) # ****** log(f"connecting with {env.secret("DB_PASSWORD")}") # connecting with ****** ``` * Keep `.env` out of version control (add it to `.gitignore`) and commit a `.env.example` with the names and harmless values instead. * On servers, prefer real environment variables or your platform's secret store, and run with `--allow-env=NAME1,NAME2` so the program can read only what it needs. * `Secret` values compare in constant time, so checking an API key with `==` does not leak it through timing. # Debugging Apsi has a real debugger: breakpoints, stepping, the call stack, variables and tensors — in VS Code and the other VS Code-family editors (F5) or in the terminal. It works on the compiled engine: Apsi lines are mapped back from the generated code, so breakpoints, `step` and the stack follow your source, also through imported `.apsi` files and converted Python programs. ## In VS Code Install the Apsi extension (`apsi editors install vscode`), open a `.apsi` file, click left of a line number to set a breakpoint and press **F5**. The extension starts `apsi dap` (the Debug Adapter Protocol server) for you. * **Breakpoints:** plain, conditional (`x > 10`), hit counts (`5`, `>= 100`), and log points (`loss = {loss}`). * **Stop on uncaught errors** (on by default): the program pauses where the error happened, with all variables. * **Variables:** locals and globals of each frame; lists, dicts, records and objects expand; values can be changed while paused. * **Tensors** show shape, dtype, min/max/mean and the first values instead of a wall of numbers. * **Watch / Debug Console:** any Apsi expression in the paused frame. * Program output appears in the Debug Console. Without a `launch.json`, F5 debugs the open file. With one: ```json { "type": "apsi", "request": "launch", "name": "Run app.apsi", "program": "${file}", "args": ["--epochs", "3"], "stopOnEntry": false } ``` ## In the terminal ```bash apsi debug app.apsi # stops at the first line apsi debug app.apsi -b 42 -b "57 if loss > 1" -- --epochs 3 ``` | command | | |---|---| | `c` | continue | | `n` | next line (step over) | | `s` | step into a call | | `o` | step out of the function | | `b LINE`, `b FILE:LINE`, `b LINE if COND` | add a breakpoint | | `p EXPR` | print an expression (tensors summarised) | | `l` | local variables | | `bt` | call stack | | `q` | quit | ## Without a debugger * `print(x)` of a tensor gives its shape, dtype and values. * Errors name the file, line and column and show the line with a caret, plus the chain of Apsi function calls. * `apsi check app.apsi` finds type, unit and name errors without running anything. ## Breakpoints in detail | Kind | In VS Code | In the terminal | Stops when | |---|---|---|---| | Line | click left of the line number | `b 42` or `b model.apsi:42` | the line is reached | | Conditional | right-click → *Add Conditional Breakpoint* → expression | `b 42 if loss > 1` | the expression is true in that frame | | Hit count | right-click → *Hit Count* → `5` or `>= 5`, `> 5`, `== 5`, `% 10` | — | from the 5th hit on, after the 5th, only the 5th, every 10th | | Log point | right-click → *Add Logpoint* → `epoch {epoch}: loss = {loss}` | — | never: the message is printed and the program continues | | Uncaught error | *Breakpoints* panel → *Uncaught errors* (on by default) | always on | an error is about to end the program | Conditions and log-point expressions are Apsi expressions evaluated in the paused frame: names, attributes, indexing, arithmetic, comparisons and calls. A condition that cannot be evaluated (a typo, a name that does not exist yet) stops at the breakpoint, so you see the problem instead of silently missing the stop. ## What the variables view shows * **Locals and globals** of every frame in the call stack. Module-level globals are limited to the names your program declared, not everything imported. * **Tensors and arrays:** `tensor (64, 128) float32 min -0.91 max 1.2 mean 0.003 [0.12, -0.04, …]`. Expand them to see rows. * **Quantities with units** show as Apsi prints them (`9.81 m/s^2`). * **Changing a value:** double-click it in the VS Code variables view and type any Apsi expression; the program continues with the new value. ## Debugging servers, tests and converted Python * **Web servers:** set a breakpoint inside a route and send a request; the request waits while you are paused. * **Tests:** `apsi debug tests/test_model.apsi` runs the test file under the debugger; a failing `assert` stops with its values visible when *Uncaught errors* is on. * **Converted Python:** `apsi convert script.py` then `apsi debug script.apsi`. Breakpoints and the stack refer to the Apsi lines, which match the Python structure line for line. * **Permissions** are the same as for `apsi run`: pass `--allow-read`, `--allow-net` and so on after the file. ## Cost The debugger traces only frames of Apsi code (not the standard library or NumPy), and only while it is attached. `apsi run` has no tracing at all, so there is no reason to remove breakpoints or debug code before deploying. # Editors and developer tools Apsi works in the editors people already use. Each one gets Apsi's own colours, the Apsi file icon where the editor supports one, run/test commands and — through the language server — live errors, hover, go to definition, completion and formatting. ## Supported editors | Editor | How Apsi gets there | What you get | |---|---|---| | **Visual Studio Code** | setup wizard / `apsi editors install vscode` | everything: colours, icon, themes, snippets, run/test/REPL commands, tasks, language server | | **Cursor** | setup wizard / `apsi editors install cursor` | the same extension as VS Code | | **Windsurf** | setup wizard / `apsi editors install windsurf` | the same extension as VS Code | | **VSCodium**, **VS Code Insiders**, **Trae** | setup wizard / `apsi editors install vscodium` … | the same extension as VS Code | | **Sublime Text 3/4** | setup wizard / `apsi editors install sublime` | colours, build system (`Ctrl+B`: run, test, check, format, bench), clickable errors, comments, indentation, Goto Symbol, snippets, command palette; language server with the LSP package | | **Neovim** | setup wizard / `apsi editors install neovim` | filetype, colours, indentation, `:make`, mappings, and the **language server starts automatically** | | **Vim** | setup wizard / `apsi editors install vim` | filetype, colours, indentation, `:make`, mappings | | **Notepad++** (Windows) | setup wizard / `apsi editors install notepadpp` | Apsi language with its own colours, picked automatically for `.apsi` files | | **JetBrains IDEs** (IntelliJ IDEA, PyCharm, CLion, WebStorm, …) | one manual step (below) | colours, comments and snippets from the TextMate bundle; language server through LSP4IJ | | **Helix**, **Zed**, **Emacs** | one manual step (below) | language server; Emacs also gets `apsi-mode` with colours and indentation | ### `apsi editors` ```text apsi editors list editors: found on this computer? Apsi installed? apsi editors install add Apsi to every editor that was found apsi editors install cursor vscode only these (ids: vscode vscode-insiders cursor windsurf vscodium trae sublime neovim vim notepadpp, or all) apsi editors uninstall [NAMES] remove it again apsi editors path the folder that holds the integrations (…/editors) ``` The setup wizard runs the same command: its **Code editor support** component lists the editors it found, and the uninstaller removes Apsi from them again. Nothing outside each editor's own extension/package folder is touched. Restart editors that were open so they load Apsi. How each one is installed: - **VS Code family** — with the editor's command-line tool (`code`, `cursor`, `windsurf`, `codium`, …) when it is available; otherwise the extension is unpacked straight into the editor's extension folder (`~/.vscode/extensions`, `~/.cursor/extensions`, `~/.windsurf/extensions`, `~/.vscode-oss/extensions`, …), so it also works when the CLI is not on PATH. - **Sublime Text** — `Packages/Apsi` in the Sublime data folder. - **Neovim / Vim** — a native package in `…/site/pack/apsi/start/apsi` (Neovim) or `~/.vim/pack/apsi/start/apsi` (`~/vimfiles/…` on Windows); no plugin manager and no change to your `init.lua`/`.vimrc`. - **Notepad++** — `%APPDATA%\Notepad++\userDefineLangs\Apsi.xml`. ### Manual steps | Editor | Step | |---|---| | JetBrains | *Settings → Editor → TextMate Bundles → +* → choose `/editors/textmate/Apsi.tmbundle`. For live errors install the **LSP4IJ** plugin and add a language server with the command `apsi lsp` for `*.apsi`. | | Helix | append `editors/helix/languages.toml` to `~/.config/helix/languages.toml` (`%AppData%\helix\` on Windows) | | Zed | merge `editors/zed/settings.json` into Zed's `settings.json` | | Emacs | `(load "/editors/emacs/apsi-mode.el")` in your init file; `M-x eglot` starts the language server | `apsi editors` prints these steps with the real paths of your installation. ### Neovim and Vim details | Key | Does | |---|---| | `r` | run the file | | `t` | test the file | | `c` | check the file (`:make`, errors in the quickfix list) | | `f` | format the file | | `i` | Apsi REPL in a terminal | Neovim settings: `vim.g.apsi_lsp = false` keeps the language server off; `vim.g.apsi_command = { "py", "-3", "-m", "apsi" }` starts Apsi another way. `:help apsi` has the details. ### Sublime Text details `Ctrl+B` runs the file, `Ctrl+Shift+B` chooses *Test*, *Check*, *Format*, *Benchmark*, *Run Project* or *Test Project*, `F4` jumps to the next error. For the language server, install the **LSP** package and add `{ "clients": { "apsi": { "enabled": true, "command": ["apsi", "lsp"], "selector": "source.apsi" } } }` to its settings. ### One grammar everywhere `editors/vscode/syntaxes/apsi.tmLanguage.json` and `editors/vscode/snippets/apsi.json` are the source of truth; `python tools/build_editors.py` regenerates the Sublime Text and TextMate/JetBrains versions from them, so every editor colours Apsi the same way. Vim, Notepad++ and Emacs use hand-written equivalents. ## Language server (`apsi lsp`) Apsi has a standard LSP language server built in, so it works with almost every editor: - **errors while you type**: unit mismatches, undeclared names, assignments to `let`, wrong arguments, `evolve` rules, syntax errors - **hover**: a variable shows its physical type, e.g. `let speed: Velocity [m/s]`; a function shows its signature - **go to definition** - **completion**: names in the file, built-ins, modules, physical types, and **unit names inside `[...]`** - **format document** (the same formatter as `apsi fmt`) - **outline** (document symbols) ## VS Code, Cursor, Windsurf, VSCodium Everything in this section applies to all editors of the VS Code family — they share one extension. The setup wizard installs it automatically into every one it finds. To install it by hand: Extensions → `…` → **Install from VSIX…** → `dist/apsi-language-2.4.4.vsix`. The extension finds Apsi by itself, in this order: `apsi.command` from Settings, the wizard's install (`%LOCALAPPDATA%\Programs\Apsi` / `~/.local/share/apsi`), the script install (`%LOCALAPPDATA%\Apsi` / `~/.apsi`), `apsi` on PATH, `py -3 -m apsi`, `python -m apsi`. Set it yourself with Settings → `apsi.command`, e.g. `["C:\\tools\\python\\python.exe", "-m", "apsi"]`. ### What you get | Feature | Details | |---|---| | Syntax colours | keywords, `let`/`var`/`fn`/`struct`, definitions vs. calls, built-ins, parameters, types, **units in their own colour** (`9.81[m/s^2]`, `as [km]`), `±`, `x'` derivatives in `evolve`, f-string interpolation, decorators, `TODO` in comments; Apsi code blocks in Markdown are coloured too | | Themes | **Apsi Dark** and **Apsi Light** (optional — every other theme works as well) | | File icon | the Apsi logo, letters only, on a transparent background | | Run / test | ▶ button in the editor title bar, status-bar button, right-click menus in the editor and the Explorer | | REPL | **Apsi: Open REPL**; `Shift+Enter` sends the selection or the current line (blocks are sent whole) | | Tasks | `Terminal → Run Task → apsi` lists *test*, *check*, *format*, *run project* and every task from `apsi.toml`; errors in the output appear in the Problems panel (`$apsi` problem matcher) | | Projects | **Apsi: New Project…** creates a project from a template and opens it | | Python → Apsi | **Apsi: Convert Python File to Apsi** (right-click a `.py` file or folder, or the editor title bar) — the result opens beside it | | Snippets | `fn`, `fnb`, `struct`, `test`, `for`, `fore`, `if`, `ife`, `evolve`, `grad`, `train`, `route`, `main`, `use`, `import`, `@prec`, `pf`, … | | Language server | everything in the list above | ### Commands and keys | Command | Key | |---|---| | Apsi: Run File | `Ctrl+F5` or `Ctrl+Alt+N` | | Apsi: Run File with Arguments… | – (remembers the last arguments per file) | | Apsi: Test File | `Ctrl+Alt+T` | | Apsi: Open REPL | `Ctrl+Alt+R` | | Apsi: Run Selection / Line in REPL | `Shift+Enter` | | Apsi: Load File into REPL | – | | Apsi: Test Project · Check File · Watch File · Benchmark File · Show Permissions the File Needs | – | | Apsi: Run Project Task… | – (pick from `apsi.toml` `[tasks]`) | | Apsi: Run Apsi Command… | – (type any command line, e.g. `pkg list`) | | Apsi: Show Documentation for Symbol | – (`apsi doc` for the word under the cursor) | | Apsi: New Project… · Restart Language Server · Show Language Server Output | – | ### Settings | Setting | Default | Meaning | |---|---|---| | `apsi.command` | `[]` (auto-detect) | how to start Apsi | | `apsi.run.arguments` | `[]` | extra flags for Run and Test, e.g. `["--allow-net"]` | | `apsi.run.saveBeforeRun` | `true` | save before running | | `apsi.run.clearTerminal` | `false` | clear the terminal before each run | | `apsi.languageServer.enabled` | `true` | diagnostics, hover, completion, formatting | | `apsi.statusBar.enabled` | `true` | version and Run button in the status bar | ### Tasks in `tasks.json` ```json { "version": "2.0.0", "tasks": [ { "type": "apsi", "command": "run", "file": "src/main.apsi", "args": ["--allow-net"], "label": "serve" }, { "type": "apsi", "command": "task", "task": "ci", "label": "ci", "group": "test" } ] } ``` `apsi new` writes a ready `.vscode/tasks.json` (run, test, check, format, watch) for every new project. Build the `.vsix` from source: ```bash cd editors/vscode && npm install && npx @vscode/vsce package ``` --- ## Formatter (`apsi fmt`) ```bash apsi fmt # every .apsi file under the current folder apsi fmt examples # one folder apsi fmt --check src # check only (for CI): exit code 1 when a file would change ``` - 4-space indentation, a space after `,`, spaces around `= == -> => |> ±`, two spaces before an end-of-line comment - unit literals (`9.81[m/s^2]`), ranges (`0..10`) and keyword arguments (`f(x=1)`) have no spaces - **100 % safe**: a file is written only when its syntax tree after formatting is identical to the one before ## Other highlighters The TextMate grammar `editors/vscode/syntaxes/apsi.tmLanguage.json` also works in Sublime Text, JetBrains (TextMate Bundles plugin), GitHub Linguist and Shiki (for highlighting on a website). # Building Apsi 2.4.4 from source ## Requirements | | Windows 10/11 | Linux | |---|---|---| | Python 3.12+ | `winget install Python.Python.3.12` | `sudo apt install python3 python3-venv` | | Node.js 20+ (installer only) | `winget install OpenJS.NodeJS.LTS` | `sudo apt install nodejs npm` (or nodejs.org) | | NSIS + zip (classic .exe/.deb/zip only) | – | `sudo apt install nsis zip` | ## One command ```powershell # Windows (PowerShell, in the source folder) powershell -ExecutionPolicy Bypass -File setup.ps1 # .venv + Apsi + tests + malware scan of examples powershell -ExecutionPolicy Bypass -File setup.ps1 -Installer # + installer\release\ApsiSetup-2.4.4-x64.exe powershell -ExecutionPolicy Bypass -File setup.ps1 -Libs ai,gpu # + libraries ``` ```bash # Linux ./setup.sh # .venv + Apsi + tests + malware scan of examples ./setup.sh --installer # + installer/release/*.AppImage ./setup.sh --release # + dist/: wheel, .deb, Windows zip, NSIS exe, SHA256SUMS ./setup.sh --libs ai,gpu # + libraries ``` Linux release files that need **no internet and no Node.js** (any Linux, also WSL): ```bash python3 packaging/make_linux_installer.py dist/linux # dist/linux/ApsiSetup-2.4.4-linux.sh (self-extracting installer) python3 packaging/make_wheel.py dist/linux # dist/linux/apsi-2.4.4-py3-none-any.whl (standard library only) SKIP_WINDOWS=1 SKIP_ELECTRON=1 sh packaging/build_all.sh # dist/apsi_2.4.4_all.deb + wheel ``` The AppImage wizard and the portable `tar.gz` with its own Python download Electron and Python, so they need internet: `bash packaging/build_linux_release.sh` builds all Linux files into `dist/linux/`. ## Libraries (`apsi pkg`) | Bundle | Installs | |---|---| | `gpu` | cupy-cuda12x (NVIDIA, compute 7.5+: GTX 1660 and newer) | | `onnx` / `onnx-gpu` | onnxruntime / onnxruntime-gpu | | `torch-auto` | PyTorch, the build that fits the machine (by NVIDIA driver version; ROCm on AMD/Linux; else CPU) | | `torch` | PyTorch, CPU build | | `torch-cuda` · `torch-cu128` · `torch-cu124` · `torch-cu118` | PyTorch with CUDA 12.6 (default) · 12.8 (RTX 50) · 12.4 · 11.8 — one build at a time; an installed build is kept, another one is replaced | | `transformers` | transformers, tokenizers, safetensors, huggingface_hub | | `sci` · `data` · `plot` · `crypto` | scipy · pandas + pyarrow · matplotlib · cryptography | | `audit` | pip-audit | | groups | `ai` = onnx torch-auto transformers · `ai-gpu` = gpu onnx-gpu torch-cuda transformers · `science` = sci data plot · `all` | ``` apsi pkg list # what is available and installed apsi pkg install ai # binary wheels only, over HTTPS apsi pkg install some-package # any PyPI package apsi pkg lock # apsi.lock with exact versions + sha256 apsi pkg install -r apsi.lock # reproducible install, hashes enforced apsi pkg audit # conflicts + known vulnerabilities apsi pkg remove torch ``` ## Step by step (what the scripts do) ```bash python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 pip install -r requirements.txt && pip install -e . python -c "from apsi.security import write_manifest; write_manifest()" apsi test tests/apsi examples --allow-all && python -m unittest discover -s tests cd installer python ../packaging/build_payload.py --platform windows --out payload # or --platform linux npm ci && npm test && npm run build:ui npx electron-builder --win portable --x64 # Windows → release/ApsiSetup-2.4.4-x64.exe npx electron-builder --linux AppImage # Linux → release/*.AppImage npm run dev # UI preview in a browser (mock system) ``` The payload contains a relocatable CPython 3.12 (python-build-standalone), numpy, Apsi, the examples, the docs, the VS Code extension and the brand icons. The build downloads the runtime once into `packaging/.cache/`. ## Signing (recommended for public releases) ```powershell $env:CSC_LINK = "C:\keys\apsi-codesign.pfx"; $env:CSC_KEY_PASSWORD = "…" npm run dist:win ``` ## Unattended install (IT deployment) ``` ApsiSetup-2.4.4-x64.exe --silent [--dir PATH] [--profile sandbox|strict|standard|developer] [--no-examples] [--no-docs] [--no-editors] [--no-path] [--no-fileAssociation] [--no-startMenu] [--desktop-shortcut] [--with-gpu] [--with-onnx] [--with-torch] [--with-transformers] ApsiSetup-2.4.4-x64.exe --silent --uninstall ``` # Apsi Playground (runs in the browser) The `playground/` folder is a web page that runs Apsi in the browser with no server doing the work. It uses Pyodide (CPython compiled to WebAssembly) and numpy entirely on the visitor's machine, so it can be put on the official website as is. ## Use ```bash python tools/build_playground.py # bundle the apsi sources + examples into playground/apsi_bundle.js python -m http.server -d playground 8000 # open http://localhost:8000 ``` To publish, upload `playground/index.html` and `playground/apsi_bundle.js` to any static host such as GitHub Pages, Netlify, Cloudflare Pages or Vercel. ## Features - pick an example from the menu (examples that need Python, C or the network are left out) - run with `Ctrl+Enter`; output, checker errors (with their location) and `test` results are shown - follows the system's dark/light mode and works on phones - **safe**: user code runs sandboxed (`permissions = {}`) and cannot read files, call Python/C or use the network ## What runs in the browser | Works | Not available in the browser | |---|---| | the whole language, both engines, the checker and `test` blocks | files, the network, Python libraries and C code (the sandbox has no permissions) | | units, uncertainty, exact fractions, number formats (FP64 … FP4) | the PyTorch backend and GPUs (`backend="auto"` stays on NumPy) | | `use nn`, `use layers` (every layer, `dag`, transformers, GNNs, `fit`), `use sci`, `use linalg`, `evolve` | `use web` servers, `use parallel` worker threads | | autodiff of any order | `apsi build`, the compile cache on disk (the in-memory cache still works) | Speed: Pyodide runs Python and NumPy as WebAssembly, so programs run slower than on the desktop. The first visit downloads Pyodide and NumPy (several MB); the browser caches them for later visits. ## Updating Run `python tools/build_playground.py` again after changing the Apsi sources or the examples; it rebuilds `playground/apsi_bundle.js` (45 modules and every example that runs in the browser). ## Embedding in another page Call `apsi.playground.run(code)` through Pyodide; it returns a dict `{output, error, ok, tests}`.