Skip to content

Builders

Builders provide a fluent API for constructing optimisation problems. Choose the appropriate builder based on your problem type.

Overview

Builder Use Case Example
ScalarBuilder Direct function optimisation Rosenbrock, Rastrigin
DiffsolBuilder ODE fitting with DiffSL Parameter identification
VectorBuilder Custom solver integration JAX, Julia, external simulators

ScalarBuilder

ScalarBuilder

High-level builder for optimisation Problem instances exposed to Python.

__new__

__new__()

Create an empty builder with no objective, parameters, or default optimiser.

with_objective

with_objective(obj)

Attach the objective function callable executed during optimisation.

with_parameter

with_parameter(name, initial_value, bounds=None)

Register a named optimisation variable in the order it appears in vectors.

build

build()

Finalize the builder into an executable Problem.

Example Usage

import numpy as np
import diffid as chron

def rosenbrock(x):
    return np.asarray([(1 - x[0])**2 + 100*(x[1] - x[0]**2)**2])

builder = (
    diffid.ScalarBuilder()
    .with_objective(rosenbrock)
    .with_parameter("x", 1.5)
    .with_parameter("y", -1.5)
)
problem = builder.build()
result = problem.optimise()

When to Use

  • You have a Python function to minimise directly
  • No differential equations involved
  • Simple parameter optimisation or test functions

DiffsolBuilder

DiffsolBuilder

Differential equation solver builder.

__new__

__new__()

Create an empty differential solver builder.

with_diffsl

with_diffsl(dsl)

Register the DiffSL program describing the system dynamics.

with_data

with_data(data)

Attach observed data used to fit the differential equation.

The first column must contain the time samples (t_span) and the remaining columns the observed trajectories.

with_parameter

with_parameter(name, initial_value, bounds=None)

Register a named optimisation variable in the order it appears in vectors.

with_backend

with_backend(backend)

Choose whether to use dense or sparse diffusion solvers.

build

build()

Create a Problem representing the differential solver model.

Example Usage

import numpy as np
import diffid as chron

dsl = """
in { r = 1, k = 1 }
u_i { y = 0.1 }
F_i { (r * y) * (1 - (y / k)) }
"""

t = np.linspace(0.0, 5.0, 51)
observations = np.exp(-1.3 * t)
data = np.column_stack((t, observations))

builder = (
    diffid.DiffsolBuilder()
    .with_diffsl(dsl)
    .with_data(data)
    .with_parameter("k", 1.0)
    .with_backend("dense")
)
problem = builder.build()
optimiser = diffid.CMAES().with_max_iter(1000)
result = optimiser.run(problem, [0.5, 0.5])

Backend Options

  • "dense" (default): For systems with < 100 variables, dense Jacobian
  • "sparse": For large systems (> 100 variables), sparse Jacobian

DiffSL Syntax

DiffSL is a domain-specific language for ODEs:

in_i { param1 = default1, param2 = default 2 }           # Parameters to fit with defaults
u_i { state1 = init1 }          # Initial conditions
F_i { derivative_expr }         # dy/dt expressions
out_i { state1, state2 }        # Optional: output variables

When to Use

  • Fitting ODE parameters to time-series data
  • Using Diffid's built-in high-performance solver
  • Models expressible in DiffSL syntax

VectorBuilder

VectorBuilder

Time-series problem builder for vector-valued objectives.

__new__

__new__()

Create an empty vector problem builder.

with_objective

with_objective(objective)

Register a callable that produces predictions matching the data shape.

The callable should accept a parameter vector and return a numpy array of the same shape as the observed data.

with_data

with_data(data)

Attach observed data used to fit the model.

The data should be a 1D numpy array. The shape will be inferred from the data length.

with_parameter

with_parameter(name, initial_value, bounds=None)

Register a named optimisation variable in the order it appears in vectors.

build

build()

Create a Problem representing the vector optimisation model.

Example Usage

import numpy as np
import diffid as chron

def custom_solver(params):
    """Your custom ODE solver (e.g., using JAX/Diffrax)."""
    # Solve ODE with params
    # Return predictions at observation times
    return predictions  # NumPy array

# Observed data
t = np.linspace(0, 10, 100)
observations = ...  # Your experimental data
data = np.column_stack((t, observations))

builder = (
    diffid.VectorBuilder()
    .with_objective(custom_solver)
    .with_data(data)
    .with_parameter("alpha", 1.0)
    .with_parameter("beta", 0.5)
)
problem = builder.build()
result = problem.optimise()

Callable Requirements

Your callable must:

  1. Accept a NumPy array of parameters
  2. Return a NumPy array of predictions
  3. Predictions must match observation times in the data

When to Use

  • Need specific solver features (stiff solvers, event detection, etc.)
  • Using JAX/Diffrax for automatic differentiation
  • Using Julia/DifferentialEquations.jl via diffeqpy
  • Custom forward models beyond ODEs (PDEs, agent-based models, etc.)

See the Custom Solvers Guide for examples with Diffrax and DifferentialEquations.jl.


Problem

Problem

Executable optimisation problem wrapping the Diffid core implementation.

evaluate

evaluate(x)

Evaluate the configured objective function at x.

evaluate_gradient

evaluate_gradient(x)

Evaluate the gradient of the objective function at x if available.

optimise

optimise(initial=None, optimiser=None)

Solve the problem starting from initial using the supplied optimiser.

sample

sample(initial=None, sampler=None)

Sample from the problem starting from initial using the supplied sampler.

get_config

get_config(_key)

Return the numeric configuration value stored under key if present.

dimension

dimension()

Return the number of parameters the problem expects.

bounds

bounds()

Return the parameter bounds for the problem as a list of (lower, upper) tuples.

default_parameters

default_parameters()

Return the default parameter vector implied by the builder.

config

config()

Return a copy of the problem configuration dictionary.

__call__

__call__(x)

Call the problem as a function (shorthand for evaluate).

Allows using problem(x) instead of problem.evaluate(x).

__repr__

__repr__()

Return a detailed string representation of the problem.

__str__

__str__()

Return a concise string representation of the problem.

The Problem class is created by calling .build() on a builder. It represents a fully configured optimisation problem.

Common Methods

# Optimise with default settings (Nelder-Mead)
result = problem.optimise()

# Evaluate objective at specific parameters
value = problem.evaluate(params)

Common Patterns

Chaining Methods

Builders use a fluent interface - chain methods in any order:

builder = (
    diffid.ScalarBuilder()
    .with_objective(func)
    .with_parameter("x", 1.0)
    .with_parameter("y", 2.0)
    .with_cost_metric(diffid.RMSE())
)

Reusing Builders

Builders are immutable - each method returns a new builder:

base = diffid.ScalarBuilder().with_objective(func)

problem1 = base.with_parameter("x", 1.0).build()
problem2 = base.with_parameter("x", 2.0).build()  # Different initial guess

Parameter Order Matters

Parameters are indexed in the order they're added:

builder = (
    diffid.ScalarBuilder()
    .with_parameter("y", 2.0)  # Index 0
    .with_parameter("x", 1.0)  # Index 1
)

result = problem.optimise()
print(result.x)  # [optimal_y, optimal_x]

See Also