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