Skip to content

Installation

Scaly supports Python 3.12 or newer on Linux and macOS, and on Windows for x86-64 processors only. Numerical function calls compile generated C, so the Python package needs access to a C compiler even when the application itself is entirely Python.

Intel Macs

Scaly 0.1.x is the last release line with solver wheels for Intel Macs. From 0.2.0, Scaly supports macOS on Apple silicon only.

The core package and solver plugins

The core package provides symbolic expressions, functions, derivatives, and C generation. It can be installed in a uv project or a Python environment:

uv add scaly
# or
pip install scaly

Optimization solvers are optional packages:

Solver uv command pip command
PIQP, for convex quadratic problems uv add "scaly[piqp]" pip install "scaly[piqp]"
IPOPT, for nonlinear problems uv add "scaly[ipopt]" pip install "scaly[ipopt]"
Scaly SQP, for nonlinear problems uv add "scaly[sqp]" pip install "scaly[sqp]"
All three uv add "scaly[solvers]" pip install "scaly[solvers]"

A solver plugin connects the symbolic problem to a native optimization library. The solver wheels include those native libraries. If no wheel matches the platform, a source build needs additional tools, described in Contributing. The backend guide compares the supported problems and solver options.

Compiler selection

On Linux and macOS, Scaly normally uses cc from PATH. Apple's command-line developer tools provide it on macOS, and build-essential provides it on Debian and Ubuntu. On Windows, the core package depends on zig-bin, which installs zig cc into the Python environment. Scaly uses it without Visual Studio, MSYS2 or a separate MinGW installation.

On Linux and macOS, the toolchain extra installs the same compiler as a Python package:

uv add "scaly[toolchain]"
# or
pip install "scaly[toolchain]"

It brings zig cc 0.17 from the zig-bin package, or from ziglang on Intel macOS, where zig-bin has no wheel. zig cc is a Clang driver that ships with its own C runtime support, so the same Clang compiles numerical functions on every system. When either package is installed, Scaly prefers it to CC and cc.

The first compilation on a machine takes longer while zig builds that runtime support into its own cache. Scaly compiles for the host processor, so zig builds it once per processor model and zig version, and builds it again when its cache moves to a machine with another processor. It takes 2 to 3 seconds on Linux and macOS. On Windows, where zig also builds a MinGW-w64 C runtime, the first compilation of a small function took about 13 seconds on a recent desktop processor, and about 0.4 seconds in later processes. On a four-core Windows runner of GitHub Actions it took 1.3 to 1.5 minutes, so a test with a time limit should compile one function first.

SCALY_CC selects a different compiler and takes precedence over all of these. If it is unset and neither package is installed, Scaly respects CC before falling back to cc.

For example, this selects Clang for one process:

SCALY_CC=clang uv run scaly_toolchain

Tested environments

Scaly is tested every night in these environments, with Python 3.14 and, on Ubuntu 24.04, also 3.12. Other compilers and systems may work, but they are not tested.

System Architectures C compilers
Ubuntu 22.04 x86-64, arm64 GCC 11, and Clang 14 on x86-64
Ubuntu 24.04 x86-64, arm64 GCC 13, and Clang 18 and zig cc 0.17 on x86-64
Ubuntu 26.04 x86-64, arm64 GCC 15, and Clang 22 on x86-64
macOS 15 arm64, x86-64 Apple Clang 17 (Xcode 16.4), and zig cc 0.17 on arm64
macOS 26 arm64, x86-64 Apple Clang 21 (Xcode 26)
Windows Server 2025 x86-64 zig cc 0.17

The selected executable must exist. An invalid override does not fall back to the system compiler. Environment variables describes the compiler and cache settings.

Reading the toolchain report

scaly_toolchain reports the selected compiler, the compiled-function cache, the native compilation settings, and the installed solver libraries:

uv run scaly_toolchain

In an environment installed with pip, the command is scaly_toolchain. An excerpt from a Linux installation with IPOPT available looks like this. Paths are shortened, and other installed solvers are omitted:

Scaly native toolchain
  cache root: /home/user/.cache/scaly/jit
  cc: /usr/bin/cc (PATH)
  native build recipe:
/* Scaly build recipe
 * CPU baseline: host-local native CPU
 * lanes=8, dialect=gnu, vector_libm=glibc, reciprocal=False
 * Math library: glibc x86-64; tanh requires glibc >= 2.35
 * gcc -O3 -march=native -fno-math-errno -c module.c
 * clang -O3 -march=native -fno-math-errno -fveclib=libmvec -c module.c
 * Link with: -lmvec -lm
 */
  solver source: plugin
  include dirs: .../scaly_ipopt/include, ...
  lib dirs: .../scaly_ipopt/lib, ...
  ipopt: .../scaly_ipopt/lib/libipopt.so
  ipopt discoverable: True
  ipopt loadable: True
  ...
  JIT solver flags: -I... -L... -Wl,-rpath,... -lipopt ...

The native build recipe describes the detected CPU and vector math settings. Its gcc and clang lines are the suggested commands for building exported code. Numerical evaluation from Python uses the same CPU and math flags but compiles with SCALY_CC_OPT, -O2 by default, as described in environment variables. Lane width and library selection vary by machine. The code generation guide explains these choices and how to select them for exported code.

discoverable means Scaly found the library and its C headers. loadable also checks that the operating system can load the library. A library can be present but fail to load because one of its native dependencies is unavailable. The final line reports the include and link flags used for solver compilation, not all optimization flags used to compile a model.

A cc: <missing> line prevents numerical evaluation of any function. A missing solver library only affects functions that use that solver. For an installed plugin, failure to locate or load its native library raises sc.SolverLibraryError. Missing compiler and solver-library problems can therefore be diagnosed separately.

A numerical check

Importing scaly alone does not check C compilation. Evaluating this function checks the numerical path as well:

import numpy as np
import scaly as sc


@sc.function(sc.arg("x", 3), outputs=sc.arg("energy"))
def energy(x: sc.Expr) -> sc.Expr:
    return sc.sumsqr(x)


value = energy(np.array([1.0, 2.0, 3.0]))
np.testing.assert_allclose(value, 14.0)
print(value)
# 14.0

This example needs no solver plugin. Getting started introduces the symbolic model and adds an optimization solve.