Your optimization code should read like your math. With Optyx, x + y >= 1 is exactly that—not a lambda buried in a constraint dictionary.
1.2 Why Another Optimization Library?
Python has excellent tools. SciPy provides algorithms. CVXPY handles convex problems elegantly. Pyomo scales to industrial applications.
Optyx takes a different path: radical simplicity.
We believe most optimization code is harder to write than it needs to be. Optyx is for developers who want to:
Write problems as they think them — x**2 + y**2 not lambda v: v[0]**2 + v[1]**2
Never compute a gradient by hand — symbolic autodiff handles it
Skip the solver configuration — sensible defaults, automatic solver selection
1.2.1 Being Honest
Optyx is young and opinionated. It’s not a replacement for specialized tools:
Need large-scale MILP with cutting planes? → Use Pyomo or Gurobi
Need convex guarantees? → Use CVXPY
Need maximum performance? → Use raw solver APIs
Optyx does support MILP via HiGHS, sparse LPs with 100,000+ variables, and solver callbacks. But if you need industrial-grade MIP with custom branching strategies, a dedicated solver is the right choice.
1.3 What You Can Do
from optyx import Variable, Problem# Find the minimum of Rosenbrock function in a boxx = Variable("x", lb=-2, ub=2)y = Variable("y", lb=-2, ub=2)rosenbrock =100*(y - x**2)**2+ (1- x)**2solution = ( Problem("rosenbrock") .minimize(rosenbrock) .solve())print(f"Minimum at ({solution['x']:.4f}, {solution['y']:.4f})")print(f"Objective value: {solution.objective_value:.6f}")
Minimum at (1.0000, 1.0000)
Objective value: 0.000000
1.3.1 Portfolio Optimization
from optyx import Variable, Problem# Three assets with position limitstech = Variable("tech", lb=0, ub=0.4)energy = Variable("energy", lb=0, ub=0.4) finance = Variable("finance", lb=0, ub=0.4)# Maximize return while controlling riskexpected_return =0.12*tech +0.08*energy +0.10*financerisk =0.04*tech**2+0.02*energy**2+0.03*finance**2solution = ( Problem("portfolio") .minimize(risk) .subject_to((tech + energy + finance).eq(1)) # fully invested .subject_to(expected_return >=0.095) # target return .solve())print(f"Allocation: tech={solution['tech']:.1%}, energy={solution['energy']:.1%}, finance={solution['finance']:.1%}")print(f"Expected return: {0.12*solution['tech'] +0.08*solution['energy'] +0.10*solution['finance']:.1%}")print(f"Portfolio risk: {solution.objective_value:.4f}")
Unlike black-box solvers, Optyx lets you see exactly what’s happening:
from optyx import Variablex = Variable("x")y = Variable("y")# Build an expressionexpr = (x +2*y)**2- x*y# See its structureprint(f"Expression: {expr}")print(f"Variables: {[v.name for v in expr.get_variables()]}")print(f"Value at x=1, y=2: {expr.evaluate({'x': 1, 'y': 2})}")
Expression: (((Variable('x') + (Constant(2) * Variable('y'))) ** Constant(2)) - (Variable('x') * Variable('y')))
Variables: ['x', 'y']
Value at x=1, y=2: 23
1.4 The Core Ideas
1.4.1 Expressions are Symbolic
When you write x + y, Optyx builds a symbolic tree—not a Python value. This means:
Expressions can be inspected, differentiated, and analyzed
Optyx analyzes your problem and picks the right solver:
Linear? → HiGHS (fast industrial LP solver)
Unconstrained NLP? → L-BFGS-B
Constrained NLP? → SLSQP or trust-constr
You can override, but you rarely need to.
1.4.4 Re-solving is Fast
Changed a parameter? Optyx caches the problem structure:
# First solve compiles the problemsolution = problem.solve()# Subsequent solves reuse compilationfor scenario in scenarios: solution = problem.solve(x0=scenario)
Re-solves avoid rebuilding unchanged symbolic and solver structures. The actual speedup depends on problem size, solver iterations, and what changed between solves.
1.5 Under the Hood: A Glimpse
Optyx isn’t magic—it’s a SciPy wrapper with a symbolic frontend. Here’s the 30-second version of what happens when you solve a problem:
Your Code What Optyx Builds What Runs
────────── ───────────────── ─────────
x**2 + y**2 → Expression Tree → SciPy's minimize()
Add
/ \
Power Power
/ \ / \
x 2 y 2
Step 1: Build the Tree — Python operators (+, *, **) are overloaded to construct a symbolic expression tree instead of computing values.
Step 2: Walk for Gradients — Each node knows its derivative rule. Power(x, 2) knows ∂/∂x = 2x. The tree is walked to compute exact gradients via the chain rule.
Step 3: Compile to Callables — The tree is compiled into fast Python functions that SciPy can call repeatedly during optimization.
Step 4: Call SciPy — Optyx passes your objective, gradient, and constraints to scipy.optimize.minimize() (or HiGHS for LP). The actual optimization algorithm is SciPy’s.
WarningHonest About Overhead
The symbolic layer adds model-building and compilation work. The bundled benchmarks show that the ratio to raw SciPy varies by model type and size. The payoff comes from exact generated derivatives, cached re-solves, and readable models you can maintain. Consult the versioned benchmark page rather than assuming one fixed overhead ratio.
What if you have many variables? The natural approach is loops:
from optyx import Variable, Problemimport numpy as np# 10 portfolio weightsn_assets =10weights = [Variable(f"w_{i}", lb=0, ub=1) for i inrange(n_assets)]# Expected returnsnp.random.seed(42)returns = np.random.uniform(0.05, 0.15, n_assets)# Build objective via loopexpected_return =sum(weights[i] * returns[i] for i inrange(n_assets))# Budget constraint via loopbudget =sum(weights)print(f"Created {len(weights)} variables")print(f"Objective type: {type(expected_return).__name__}")
Created 10 variables
Objective type: BinaryOp
This works for small problems. But there are limitations…
1.8 The Limitations of Loops
As problems grow, the loop approach becomes painful:
1.8.1 1. Verbose Code
# 100 variables = 100 iterationsweights = [Variable(f"w_{i}", lb=0, ub=1) for i inrange(100)]# Quadratic objective = 10,000 iterations (n²)variance =sum( weights[i] * covariance[i, j] * weights[j]for i inrange(100)for j inrange(100))
1.8.2 2. Error-Prone Indexing
# Easy to make mistakes with indicesconstraint =sum(weights[i] * data[j] for i inrange(n)) # Bug: should be data[i]
1.8.3 3. Slow Expression Building
Each loop iteration creates expression nodes. For n=100: - Linear sum: 100 additions - Quadratic form: 10,000 multiplications + 10,000 additions
This compilation overhead grows with problem size.
1.8.4 4. No Vectorized Gradients
Loop-built expressions differentiate element-by-element. NumPy-style operations could be much faster.
1.9 VectorVariable: Vectors Done Right
VectorVariable solves these problems with NumPy-like syntax:
from optyx import VectorVariableimport numpy as np# Create 10 variables in one linew = VectorVariable("w", size=10, lb=0, ub=1)print(f"Created: {w.name} with {len(w)} elements")print(f"First element: {w[0].name}")print(f"Last element: {w[-1].name}")
Created: w with 10 elements
First element: w[0]
Last element: w[9]
1.9.1 Vectorized Operations
from optyx import VectorVariable, Problemimport numpy as npnp.random.seed(42)n =10# Data as NumPy arraysreturns = np.random.uniform(0.05, 0.15, n)# Variables as VectorVariablew = VectorVariable("w", n, lb=0, ub=1)# Dot product — no loops!expected_return = returns @ w# Sum — one call!budget = w.sum()print(f"Return type: {type(expected_return).__name__}")print(f"Sum type: {type(budget).__name__}")
Return type: LinearCombination
Sum type: VectorSum
1.9.2 Full Portfolio Example
from optyx import VectorVariable, Problemimport numpy as npnp.random.seed(42)n =20# 20 assets# Problem datareturns = np.random.uniform(0.05, 0.15, n)# Decision variablesw = VectorVariable("w", n, lb=0, ub=1)# Maximize return subject to budgetsolution = ( Problem("portfolio_vector") .maximize(returns @ w) .subject_to(w.sum().eq(1)) .solve())# Extract resultsopt_weights = np.array([solution[f"w[{i}]"] for i inrange(n)])print(f"Status: {solution.status}")print(f"Max return: {solution.objective_value:.2%}")print(f"Non-zero positions: {np.sum(opt_weights >0.01)}")
Status: SolverStatus.OPTIMAL
Max return: 14.70%
Non-zero positions: 1
For portfolio variance (w.T @ Σ @ w), use w.dot(Σ @ w) for natural math-like syntax with analytic gradients:
from optyx import VectorVariable, Problemimport numpy as npnp.random.seed(42)n =10# Create positive-definite covariancedata = np.random.randn(n, 5)cov = data @ data.T /5# Decision variablesw = VectorVariable("w", n, lb=0, ub=1)# Math-like syntax: w · (Σw) = wᵀΣw with analytic gradientvariance = w.dot(cov @ w)solution = ( Problem("min_variance") .minimize(variance) .subject_to(w.sum().eq(1)) .solve())print(f"Min variance: {solution.objective_value:.6f}")
Min variance: 0.000119
NoteWhy w.dot(Σ @ w)?
A loop-built quadratic form creates n² expression nodes. w.dot(Σ @ w) uses a single MatrixVectorProduct + DotProduct node with analytic gradient: ∇(wᵀ Q w) = 2Qw. This is much faster for large n.
JAX backend — JIT-compiled autodiff for complex models
More solvers — IPOPT integration for large-scale NLP
Debugging tools — Infeasibility diagnostics and constraint analysis
Stochastic programming — Scenario-based optimization support
WarningDocumentation disclaimer
This documentation is largely generated with the assistance of AI and may contain errors. If you spot an error or a discrepancy between the documentation and Optyx’s behaviour, please create an issue.