Skip to content

Global optimization

The v0.37.0 artifacts include only differential evolution. Particle swarm, hybrid annealing and firefly require a newer coordinated release or a source checkout.

Differential evolution (DE), particle swarm (PSO) and firefly are seeded, box-bounded population searches, available in Rust, C, Go and Python. They live in finance-independent itofin-optimize; existing binding artifacts include it without another optimizer installation. Hybrid simulated annealing adds a single-chain search with local coordinate polling. All four share bounds, budgets and result types, while retaining separate update rules and controls. Node is not implemented.

Two entry points

Need API Objective
Standalone minimization Rust minimize, Python itofin.optimize.minimize, Go Minimize, context-free C optimizer Signed scalar f(x), including negative values
Existing model calibration Rust OptimizationMethod, Python itofin.optimization.{DifferentialEvolution, ParticleSwarm, HybridSimulatedAnnealing, Firefly}, Go Session.NewDifferentialEvolution / Session.NewParticleSwarm / Session.NewHybridSimulatedAnnealing / Session.NewFirefly Existing cost function's scalar value

The standalone API rejects an out-of-box x0. The calibration adapter instead clamps initially valid model parameters into the effective search box before starting; subsequent candidates still require the model constraint check.

The standalone API does not turn a scalar into a residual or minimize its absolute value. The default QuantLib-style cost function uses residual RMS; model calibration overrides it with sqrt(sum(weight[i] * error[i]²)). The adapter calls that existing scalar operation directly, preserving its semantics. This is root-RSS, not the unrooted sum of squared residuals.

Search contract

Population settings

Setting Meaning / default
Bounds One finite ordered pair per coordinate; finite interval width required
x0 Finite, inside the box; never silently projected
seed u64, default 0; every value, including zero, is deterministic
population_size Actual member count, not a dimension multiplier; default max(8, 15 * free_dimensions)
initial_population Optional full population; its row count supplies an omitted size; conflicting size is rejected
xatol Maximum coordinate spread divided by that coordinate's bound width; default 1e-6
fatol Maximum absolute objective spread; default 1e-8
maxiter / maxfev Completed generations / actual objective evaluations; defaults 1000 / 1,000,000

Explicit tolerances override Rust Common.tol, which otherwise overrides the listed defaults. Coordinate spread uses actual physical population extrema, divided by the finite bound width. With xatol=0, each physical coordinate must have exactly zero spread; rounded normalization alone cannot imply convergence. Both coordinate and objective spreads must satisfy their tolerances. PSO also requires every free-coordinate absolute normalized velocity to be at most xatol. These are heuristic stopping tests, not a global-optimum guarantee.

Differential evolution

Control Meaning / default
mutation Differential weight F, finite in (0, 2]; default 0.8
recombination Binomial crossover probability, finite in [0, 1]; default 0.9
  • Strategy: serial DE/rand/1/bin, with three distinct donors excluding the target. At least one free coordinate crosses over, even with probability zero.
  • Every generation reads frozen parent points and scores. Equal objective values accept the trial, an explicit implementation tie rule.
  • Out-of-box donor coordinates are uniformly resampled inside their bounds. There is no implicit polishing, parallel execution or general-constraint solver.

Particle swarm

Control Meaning / default
inertia Previous-velocity coefficient, finite in [0, 1]; default 0.7
cognitive Personal-best attraction, finite in [0, 4]; default 1.4
social Global-best attraction, finite in [0, 4]; default 1.4
velocity_clamp Maximum absolute box-normalized velocity, finite in (0, 1]; default 0.2
  • Serial, synchronous global-best topology: each generation reads frozen positions, personal bests and the swarm's best. All initial velocities are zero.
  • Each free coordinate draws independent uniform r1, then r2, and computes v = inertia*v + cognitive*r1*(pbest-x)/width + social*r2*(gbest-x)/width. Velocity is clipped to [-velocity_clamp, velocity_clamp]; position moves in normalized box coordinates. These defaults and policies are ours, not a promise to reproduce another PSO library or the papers' experiments.
  • Crossing positions are clipped to the exact bound, with outward velocity zeroed there (absorbing bounds). Zero velocity preserves the physical coordinate.
  • Personal/global bests update only on strict improvement. Equal values retain the earliest evaluated best. There is no neighborhood topology, inertia schedule, constriction-factor mode, polishing or parallel execution.
  • Explicit zero inertia, cognitive or social coefficients are supported. DE controls are rejected for PSO, and PSO controls are rejected for DE.

Firefly

Control Meaning / default
alpha Initial normalized random-step amplitude, finite in [0, 1]; default 0.25
beta0 Attraction coefficient at zero distance, finite in (0, 1]; default 1.0
gamma Normalized squared-distance decay coefficient, finite in [0, 1,000,000]; default 1.0
alpha_decay Random-step multiplier per completed generation, finite in (0, 1]; default 0.97
  • Lower scalar cost means brighter. Every generation freezes the old population, scores and brighter-than relations. For each firefly i, visit strictly brighter old-generation targets j in original row order, updating i sequentially. Eligibility stays frozen even if an intermediate move makes i brighter. Equal-score targets do not attract; there is no resorting.
  • Squared distance sums squared box-scaled physical coordinate differences: distance² = sum(((target_j-current_i)/width)²) over free coordinates. Attraction uses beta = beta0 * exp(-gamma * distance²). Each move adds independent alpha_t * (U - 0.5) noise per free coordinate. Boundary crossings reflect into the box, and fixed coordinates stay exact. Every proposal draws one uniform value per free coordinate, including when alpha=0; unchanged physical proposals consume no objective evaluation.
  • Fireflies without brighter targets take one random-only move. Every distinct candidate is evaluated, even if it is worse than its starting point. The result separately archives the earliest strict best across all evaluated candidates. Thus calls per generation are not generally equal to population size; all-pairs movement has quadratic population-size cost. Initialization still charges every supplied row, including duplicate points.
  • alpha_t decays after each complete generation, before its iteration callback. Termination requires coordinate and objective population spread tolerances plus random-step amplitude <= xatol. This finite-budget test is not a global-optimum guarantee. Zero noise or distance decay is supported; zero attraction is invalid.
  • No neighborhood mode, parallel evaluation, local polishing or imported runtime optimizer framework is used. DE, PSO and annealing controls are rejected.

Hybrid simulated annealing

Control Meaning / default
initial_temperature Finite positive temperature in objective units; default 1.0
cooling_rate Geometric factor, finite in (0, 1); default 0.95
step_size Uniform proposal half-width divided by box width, finite in (0, 1]; default 0.25
local_search_interval Cycles between local polls, 1..=1,000,000; default 10
local_search_steps Maximum coordinate sweeps per local poll, 1..=256; default 4
reanneal_interval Cycles between temperature resets, 1..=1,000,000; default 100
xatol / fatol Normalized unsuccessful poll radius / absolute poll-cost spread; defaults 1e-6 / 1e-8
  • One chain starts at x0; there is no population. Each free coordinate draws a uniform displacement in [-step_size, step_size] in normalized box space. Boundary-crossing proposals reflect into the box; fixed coordinates stay exact.
  • Downhill and equal-cost proposals are accepted without another random draw. An uphill proposal is accepted with probability exp(-(trial-current)/T). Rescaling an objective therefore changes the meaning of the temperature.
  • Every local-search interval, poll the best evaluated point in coordinate order, positive then negative. Polls clip to the box, skip unchanged points, and retain strict improvements immediately. This is exploratory coordinate search, not the full Hooke-Jeeves pattern-move algorithm.
  • An unsuccessful complete sweep halves the local radius. The radius starts at step_size, persists across polls and is not reset by reannealing. Local search returns the chain to its best point; reannealing also resets temperature and returns to the best, without discarding that result.
  • A completed cycle contains the proposal and any scheduled local poll. Its callback receives the best evaluated point. Cooling follows that callback; periodic reannealing then resets temperature to initial_temperature. Cooling is floored at the smallest positive f64, not zero.
  • Success requires a complete unsuccessful poll with radius <= xatol and finite observed absolute cost spread <= fatol. This reports local search resolution, not a global optimum or a classical asymptotic annealing guarantee. Explicit tolerances override Common.tol, then the listed defaults apply. Multimodal problems can successfully converge to a non-global local minimum; compare independently known objective values and more than one seed.
  • DE/PSO population controls are rejected. The same finite box, dimension and budget caps apply; population-size caps do not apply to this single-chain method.

Population initialization and shared limits

  • Random initialization includes x0 as member zero. An explicit population is validated completely and preserved without inserting x0.
  • Fixed coordinates retain their exact bound. An all-fixed problem evaluates x0 once, with zero iterations, after validating all options and any explicit population.
  • Local SplitMix64 sampling needs no third-party optimizer or runtime dependency. Repeated seeds reproduce this implementation, not another library's trajectory.

Resource limits: at most 256 coordinates, 4096 population members and 1,000,000 population coordinates. Population size is at least four. Iteration and evaluation budgets are capped at 1,000,000 and 10,000,000 respectively. All four solvers require a finite box and reject general constraints. Invalid shapes, nonfinite inputs and nonfinite interval widths are rejected before the objective runs. Python jac is unsupported; the Rust solver never calls Objective::gradient.

Results, callbacks and errors

The existing OptimizeResult reports the best evaluated point, its matching scalar value, status, success, completed generations/cycles (nit), objective calls (nfev) and zero gradient calls (njev). Exhaustion is not success.

  • Initialization consumes objective evaluations, not generations/cycles.
  • Partial initialization or a partial generation/cycle can exhaust the evaluation budget; neither increments nit nor invokes an iteration callback. Annealing proposal and local-poll evaluations share that same budget.
  • Callbacks run only after a completed generation/cycle. Cancellation waits for that boundary; it wins over iteration exhaustion at the same boundary.
  • Every nonfinite objective return terminates with the nonfinite status. No infinity barrier or penalty value replaces an exception or callback error.
  • Python exceptions and Go errors preserve their existing propagation behavior. Python callback StopIteration requests cancellation; Go uses context.Context.
  • The C scalar entry point is context-free. Go objectives run outside a Session operation, so they may update quotes and reprice using existing Session APIs. Callback buffers are borrowed only for the duration of the call.

C options and ownership

The context-free itofin_optimize_differential_evolution and itofin_optimize_particle_swarm accept separate bounds arrays and an optional flattened row-major population. ItofinDifferentialEvolutionOptions and ItofinParticleSwarmOptions contain ItofinGlobalOptions. Zero population/budget fields select defaults; has_xatol, has_fatol, has_mutation and has_recombination distinguish omitted values from explicit zero. In particular, zero crossover and zero tolerances are expressible. PSO has corresponding has_inertia, has_cognitive, has_social and has_velocity_clamp flags; zero attraction coefficients are expressible. Calibration uses itofin_differential_evolution_new/result or itofin_particle_swarm_new/result on a Session. Objective release runs exactly once, including rejected inputs and callback errors; an error return leaves the caller's result buffer untouched.

Firefly uses context-free itofin_optimize_firefly and Session itofin_firefly_new/result, with ItofinFireflyOptions containing ItofinGlobalOptions. Its coefficient presence flags preserve explicit values, including supported zeros. The callback ownership and result-buffer rules above remain unchanged.

Hybrid annealing uses context-free itofin_optimize_hybrid_simulated_annealing and Session itofin_hybrid_simulated_annealing_new/result. Bounds are passed as separate arrays; ItofinHybridSimulatedAnnealingOptions carries schedule controls, not ItofinGlobalOptions or a population. Presence flags distinguish omitted schedule controls and tolerances from explicit values. An all-zero record selects defaults. Explicit zero schedule controls are invalid; explicit zero tolerances are valid. Zero maxiter/maxfev fields select default budgets.

Calibration limits and diagnostics

Construct the calibration method with bounds in free/projected parameter order, not full model order when parameters are fixed. Existing model and additional constraints still apply; the effective box intersects their limits.

The entire search box must be feasible for the model. Every candidate is checked before pricing. A violation aborts with an error and existing model rollback, rather than performing rejection sampling or inventing a penalty. Coupled domains such as GJR stationarity or the Heston Feller condition need a carefully chosen box; simple finite bounds alone do not guarantee feasibility.

  • EndCriteria.max_iterations also limits generations/cycles. Each solver's own convergence tolerances apply; the other EndCriteria tolerances are not substituted.
  • Rust last_result(), Python last_result() and Go LastResult() expose exact diagnostics from the last solver invocation. Entering the adapter clears its previous result. Model/binding preflight failures before the adapter runs leave the previous solver diagnostics unchanged; they do not describe the failed fit.
  • Legacy EndCriteriaType has no evaluation-budget status: that outcome maps to Unknown, while the method's result retains MaxEvaluations.
  • Solver nfev excludes the model's final residual evaluation. After a completed fit, the model's reported evaluation count normally equals solver nfev + 1.

See the calibration API for method dispatch and existing weights, fixed-parameter masks and constraint arguments.

Paired executable examples

Differential evolution

The signed quadratic has analytic optimum (1.25, -0.75) and value -3. Each example checks coordinates within 1e-5 and objective value within 1e-8. The Python example also fits Hull-White volatility to one swaption, fixing mean reversion. Its one-dimensional search bound is for free sigma only, and it checks the calibration cost below 1e-6, rather than inventing a reference sigma.

"""Find a negative scalar minimum with seeded, bounded differential evolution."""

# itofin library
from itofin import Settings
from itofin.indexes import Euribor
from itofin.models import CalibrationErrorType, HullWhite, SwaptionHelper
from itofin.optimization import DifferentialEvolution, EndCriteria
from itofin.optimize import minimize
from itofin.termstructures import FlatForward
from itofin.time import Date, DayCounter, Period


def objective(x) -> float:
    """Return a signed scalar, not a residual or its absolute value."""
    return (x[0] - 1.25) ** 2 + 4.0 * (x[1] + 0.75) ** 2 - 3.0


def calibrate_hull_white() -> None:
    """Fit one volatility parameter while keeping mean reversion fixed."""
    settings = Settings()
    settings.set_evaluation_date(Date(15, 2, 2002))
    curve = FlatForward(Date(19, 2, 2002), 0.04875825, DayCounter.actual365_fixed())
    index = Euribor.six_months(curve, settings)
    model = HullWhite(curve, 0.05, 0.01)
    helper = SwaptionHelper(
        Period(1, "Years"),
        Period(5, "Years"),
        0.1148,
        index,
        Period(1, "Years"),
        DayCounter.thirty360_bond_basis(),
        DayCounter.actual360(),
        curve,
        CalibrationErrorType.RelativePriceError,
        1.0,
    )
    method = DifferentialEvolution(
        [(0.001, 0.03)],
        seed=42,
        population_size=8,
        xatol=1e-7,
        fatol=1e-10,
        maxiter=200,
        maxfev=2000,
    )
    criteria = EndCriteria(200, 50, 1e-6, 1e-8, 1e-8)
    model.calibrate([helper], method, criteria, True)
    result = method.last_result()
    assert result is not None
    assert result.success, result.message
    assert model.a() == 0.05
    assert 0.001 <= model.sigma() <= 0.03
    assert result.fun < 1e-6
    print(f"fixed a={model.a()}, calibrated sigma={model.sigma():.10f}")
    print(f"calibration cost={result.fun:.10g}, solver calls={result.nfev}")


def main() -> None:
    """Check the analytic optimum and report actual evaluation counts."""
    result = minimize(
        objective,
        [-3.0, 3.0],
        method="Differential-Evolution",
        bounds=[(-4.0, 4.0), (-4.0, 4.0)],
        options={
            "seed": 42,
            "population_size": 24,
            "xatol": 1e-7,
            "fatol": 1e-10,
        },
    )
    assert result.success, result.message
    assert abs(result.x[0] - 1.25) < 1e-5
    assert abs(result.x[1] + 0.75) < 1e-5
    assert abs(result.fun + 3.0) < 1e-8
    assert result.njev == 0
    print(f"x={result.x}, scalar minimum={result.fun:.10f}")
    print(f"generations={result.nit}, objective calls={result.nfev}")
    print(result.status, result.message)
    calibrate_hull_white()


if __name__ == "__main__":
    main()
use std::convert::Infallible;

use itofin_optimize::{
    Bounds, Common, DifferentialEvolutionOptions, GlobalOptions, Method, Problem, minimize,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut objective = |x: &[f64]| -> Result<f64, Infallible> {
        Ok((x[0] - 1.25).powi(2) + 4.0 * (x[1] + 0.75).powi(2) - 3.0)
    };
    let problem = Problem {
        x0: vec![-3.0, 3.0],
        bounds: Some(Bounds {
            lower: vec![-4.0, -4.0],
            upper: vec![4.0, 4.0],
        }),
    };
    let method = Method::DifferentialEvolution(DifferentialEvolutionOptions {
        global: GlobalOptions {
            seed: 42,
            population_size: Some(24),
            xatol: Some(1e-7),
            fatol: Some(1e-10),
            ..GlobalOptions::default()
        },
        ..DifferentialEvolutionOptions::default()
    });
    let result = minimize(&mut objective, &problem, &method, &Common::default())?;
    assert!(result.success, "{}", result.message);
    assert!((result.x[0] - 1.25).abs() < 1e-5);
    assert!((result.x[1] + 0.75).abs() < 1e-5);
    assert!((result.fun + 3.0).abs() < 1e-8);
    assert_eq!(result.njev, 0);
    println!("x={:?}, scalar minimum={:.10}", result.x, result.fun);
    println!(
        "generations={}, objective calls={}",
        result.nit, result.nfev
    );
    println!("{}", result.status);
    Ok(())
}
package main

import (
    "context"
    "fmt"
    "log"
    "math"

    itofin "github.com/benbenbang/libitofin/sdk/go"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    xatol, fatol := 1e-7, 1e-10
    objective := func(x []float64) (float64, error) {
        return math.Pow(x[0]-1.25, 2) + 4*math.Pow(x[1]+0.75, 2) - 3, nil
    }
    result, err := itofin.Minimize(context.Background(), objective, []float64{-3, 3},
        itofin.DifferentialEvolution{
            Bounds: [][2]float64{{-4, 4}, {-4, 4}},
            Seed:   42, PopulationSize: 24, XAtol: &xatol, FAtol: &fatol,
        })
    if err != nil {
        return err
    }
    if !result.Success || math.Abs(result.X[0]-1.25) >= 1e-5 ||
        math.Abs(result.X[1]+0.75) >= 1e-5 || math.Abs(result.Fun+3) >= 1e-8 || result.Njev != 0 {
        return fmt.Errorf("unexpected analytic result: %+v", result)
    }
    fmt.Printf("x=%v, scalar minimum=%.10f\n", result.X, result.Fun)
    fmt.Printf("generations=%d, objective calls=%d\n", result.Nit, result.Nfev)
    fmt.Println(result.Status, result.Message)
    return nil
}

Particle swarm

The PSO examples check the same signed quadratic and report actual generations and objective calls. Python uses method="particle-swarm"; its calibration example uses optimization.ParticleSwarm with the same Hull-White setup above. The existing root-RSS calibration cost is unchanged.

"""Find a negative scalar minimum with seeded, bounded particle swarm."""

# itofin library
from itofin import Settings
from itofin.indexes import Euribor
from itofin.models import CalibrationErrorType, HullWhite, SwaptionHelper
from itofin.optimization import EndCriteria, ParticleSwarm
from itofin.optimize import minimize
from itofin.termstructures import FlatForward
from itofin.time import Date, DayCounter, Period


def objective(x) -> float:
    """Return a signed scalar, not a residual or its absolute value."""
    return (x[0] - 1.25) ** 2 + 4.0 * (x[1] + 0.75) ** 2 - 3.0


def calibrate_hull_white() -> None:
    """Fit one volatility parameter while keeping mean reversion fixed."""
    settings = Settings()
    settings.set_evaluation_date(Date(15, 2, 2002))
    curve = FlatForward(Date(19, 2, 2002), 0.04875825, DayCounter.actual365_fixed())
    index = Euribor.six_months(curve, settings)
    model = HullWhite(curve, 0.05, 0.01)
    helper = SwaptionHelper(
        Period(1, "Years"),
        Period(5, "Years"),
        0.1148,
        index,
        Period(1, "Years"),
        DayCounter.thirty360_bond_basis(),
        DayCounter.actual360(),
        curve,
        CalibrationErrorType.RelativePriceError,
        1.0,
    )
    method = ParticleSwarm(
        [(0.001, 0.03)],
        seed=42,
        population_size=8,
        xatol=1e-7,
        fatol=1e-10,
        maxiter=1000,
        maxfev=10000,
    )
    criteria = EndCriteria(1000, 50, 1e-6, 1e-8, 1e-8)
    model.calibrate([helper], method, criteria, True)
    result = method.last_result()
    assert result is not None
    assert result.success, result.message
    assert model.a() == 0.05
    assert 0.001 <= model.sigma() <= 0.03
    assert result.fun < 1e-6
    print(f"fixed a={model.a()}, calibrated sigma={model.sigma():.10f}")
    print(f"calibration cost={result.fun:.10g}, solver calls={result.nfev}")


def main() -> None:
    """Check the analytic optimum and report actual evaluation counts."""
    calls = 0

    def counted_objective(x) -> float:
        nonlocal calls
        calls += 1
        return objective(x)

    result = minimize(
        counted_objective,
        [-3.0, 3.0],
        method="particle-swarm",
        bounds=[(-4.0, 4.0), (-4.0, 4.0)],
        options={
            "seed": 42,
            "population_size": 24,
            "xatol": 1e-7,
            "fatol": 1e-10,
        },
    )
    assert result.success, result.message
    assert abs(result.x[0] - 1.25) < 1e-5
    assert abs(result.x[1] + 0.75) < 1e-5
    assert abs(result.fun + 3.0) < 1e-8
    assert result.nfev == calls
    assert result.njev == 0
    print(f"x={result.x}, scalar minimum={result.fun:.10f}")
    print(f"generations={result.nit}, objective calls={result.nfev}")
    print(result.status, result.message)
    calibrate_hull_white()


if __name__ == "__main__":
    main()
use std::convert::Infallible;

use itofin_optimize::{
    Bounds, Common, GlobalOptions, Method, ParticleSwarmOptions, Problem, minimize,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut calls = 0;
    let mut objective = |x: &[f64]| -> Result<f64, Infallible> {
        calls += 1;
        Ok((x[0] - 1.25).powi(2) + 4.0 * (x[1] + 0.75).powi(2) - 3.0)
    };
    let problem = Problem {
        x0: vec![-3.0, 3.0],
        bounds: Some(Bounds {
            lower: vec![-4.0, -4.0],
            upper: vec![4.0, 4.0],
        }),
    };
    let method = Method::ParticleSwarm(ParticleSwarmOptions {
        global: GlobalOptions {
            seed: 42,
            population_size: Some(24),
            xatol: Some(1e-7),
            fatol: Some(1e-10),
            ..GlobalOptions::default()
        },
        ..ParticleSwarmOptions::default()
    });
    let result = minimize(&mut objective, &problem, &method, &Common::default())?;
    assert!(result.success, "{}", result.message);
    assert!((result.x[0] - 1.25).abs() < 1e-5);
    assert!((result.x[1] + 0.75).abs() < 1e-5);
    assert!((result.fun + 3.0).abs() < 1e-8);
    assert_eq!(result.nfev, calls);
    assert_eq!(result.njev, 0);
    println!("x={:?}, scalar minimum={:.10}", result.x, result.fun);
    println!(
        "generations={}, objective calls={}",
        result.nit, result.nfev
    );
    println!("{}", result.status);
    Ok(())
}
package main

import (
    "context"
    "fmt"
    "log"
    "math"

    itofin "github.com/benbenbang/libitofin/sdk/go"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    xatol, fatol := 1e-7, 1e-10
    calls := 0
    objective := func(x []float64) (float64, error) {
        calls++
        return math.Pow(x[0]-1.25, 2) + 4*math.Pow(x[1]+0.75, 2) - 3, nil
    }
    result, err := itofin.Minimize(context.Background(), objective, []float64{-3, 3},
        itofin.ParticleSwarm{
            Bounds: [][2]float64{{-4, 4}, {-4, 4}},
            Seed:   42, PopulationSize: 24, XAtol: &xatol, FAtol: &fatol,
        })
    if err != nil {
        return err
    }
    if !result.Success || math.Abs(result.X[0]-1.25) >= 1e-5 ||
        math.Abs(result.X[1]+0.75) >= 1e-5 || math.Abs(result.Fun+3) >= 1e-8 || result.Njev != 0 || result.Nfev != calls {
        return fmt.Errorf("unexpected analytic result: %+v", result)
    }
    fmt.Printf("x=%v, scalar minimum=%.10f\n", result.X, result.Fun)
    fmt.Printf("generations=%d, objective calls=%d\n", result.Nit, result.Nfev)
    fmt.Println(result.Status, result.Message)
    return nil
}

Hybrid simulated annealing

The three examples check the same signed quadratic, coordinates within 1e-5, value within 1e-8, and actual objective-call accounting. Python uses method="hybrid-simulated-annealing" and also fits the same one-parameter Hull-White model with optimization.HybridSimulatedAnnealing. The temperature acts on the model's existing root-RSS cost, not on an unrooted sum of squares or a transformed scalar.

"""Find a negative scalar minimum with seeded, bounded hybrid annealing."""

# itofin library
from itofin import Settings
from itofin.indexes import Euribor
from itofin.models import CalibrationErrorType, HullWhite, SwaptionHelper
from itofin.optimization import EndCriteria, HybridSimulatedAnnealing
from itofin.optimize import minimize
from itofin.termstructures import FlatForward
from itofin.time import Date, DayCounter, Period


def objective(x) -> float:
    """Return a signed scalar, not a residual or its absolute value."""
    return (x[0] - 1.25) ** 2 + 4.0 * (x[1] + 0.75) ** 2 - 3.0


def calibrate_hull_white() -> None:
    """Fit one volatility parameter while keeping mean reversion fixed."""
    settings = Settings()
    settings.set_evaluation_date(Date(15, 2, 2002))
    curve = FlatForward(Date(19, 2, 2002), 0.04875825, DayCounter.actual365_fixed())
    index = Euribor.six_months(curve, settings)
    model = HullWhite(curve, 0.05, 0.01)
    helper = SwaptionHelper(
        Period(1, "Years"),
        Period(5, "Years"),
        0.1148,
        index,
        Period(1, "Years"),
        DayCounter.thirty360_bond_basis(),
        DayCounter.actual360(),
        curve,
        CalibrationErrorType.RelativePriceError,
        1.0,
    )
    method = HybridSimulatedAnnealing(
        [(0.001, 0.03)],
        seed=42,
        xatol=1e-7,
        fatol=1e-10,
        maxiter=1000,
        maxfev=10000,
    )
    criteria = EndCriteria(1000, 50, 1e-6, 1e-8, 1e-8)
    model.calibrate([helper], method, criteria, True)
    result = method.last_result()
    assert result is not None
    assert result.success, result.message
    assert model.a() == 0.05
    assert 0.001 <= model.sigma() <= 0.03
    assert result.fun < 1e-6
    print(f"fixed a={model.a()}, calibrated sigma={model.sigma():.10f}")
    print(f"calibration cost={result.fun:.10g}, solver calls={result.nfev}")


def main() -> None:
    """Check the analytic optimum and report actual evaluation counts."""
    calls = 0

    def counted_objective(x) -> float:
        nonlocal calls
        calls += 1
        return objective(x)

    result = minimize(
        counted_objective,
        [-3.0, 3.0],
        method="hybrid-simulated-annealing",
        bounds=[(-4.0, 4.0), (-4.0, 4.0)],
        options={
            "seed": 42,
            "xatol": 1e-7,
            "fatol": 1e-10,
        },
    )
    assert result.success, result.message
    assert abs(result.x[0] - 1.25) < 1e-5
    assert abs(result.x[1] + 0.75) < 1e-5
    assert abs(result.fun + 3.0) < 1e-8
    assert result.nfev == calls
    assert result.njev == 0
    print(f"x={result.x}, scalar minimum={result.fun:.10f}")
    print(f"cycles={result.nit}, objective calls={result.nfev}")
    print(result.status, result.message)
    calibrate_hull_white()


if __name__ == "__main__":
    main()
use std::convert::Infallible;

use itofin_optimize::{Bounds, Common, HybridSimulatedAnnealingOptions, Method, Problem, minimize};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut calls = 0;
    let mut objective = |x: &[f64]| -> Result<f64, Infallible> {
        calls += 1;
        Ok((x[0] - 1.25).powi(2) + 4.0 * (x[1] + 0.75).powi(2) - 3.0)
    };
    let problem = Problem {
        x0: vec![-3.0, 3.0],
        bounds: Some(Bounds {
            lower: vec![-4.0, -4.0],
            upper: vec![4.0, 4.0],
        }),
    };
    let method = Method::HybridSimulatedAnnealing(HybridSimulatedAnnealingOptions {
        seed: 42,
        xatol: Some(1e-7),
        fatol: Some(1e-10),
        ..HybridSimulatedAnnealingOptions::default()
    });
    let result = minimize(&mut objective, &problem, &method, &Common::default())?;
    assert!(result.success, "{}", result.message);
    assert!((result.x[0] - 1.25).abs() < 1e-5);
    assert!((result.x[1] + 0.75).abs() < 1e-5);
    assert!((result.fun + 3.0).abs() < 1e-8);
    assert_eq!(result.nfev, calls);
    assert_eq!(result.njev, 0);
    println!("x={:?}, scalar minimum={:.10}", result.x, result.fun);
    println!("cycles={}, objective calls={}", result.nit, result.nfev);
    println!("{}", result.status);
    Ok(())
}
package main

import (
    "context"
    "fmt"
    "log"
    "math"

    itofin "github.com/benbenbang/libitofin/sdk/go"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    xatol, fatol := 1e-7, 1e-10
    calls := 0
    objective := func(x []float64) (float64, error) {
        calls++
        return math.Pow(x[0]-1.25, 2) + 4*math.Pow(x[1]+0.75, 2) - 3, nil
    }
    result, err := itofin.Minimize(context.Background(), objective, []float64{-3, 3},
        itofin.HybridSimulatedAnnealing{
            Bounds: [][2]float64{{-4, 4}, {-4, 4}},
            Seed:   42, XAtol: &xatol, FAtol: &fatol,
        })
    if err != nil {
        return err
    }
    if !result.Success || math.Abs(result.X[0]-1.25) >= 1e-5 ||
        math.Abs(result.X[1]+0.75) >= 1e-5 || math.Abs(result.Fun+3) >= 1e-8 || result.Njev != 0 || result.Nfev != calls {
        return fmt.Errorf("unexpected analytic result: %+v", result)
    }
    fmt.Printf("x=%v, scalar minimum=%.10f\n", result.X, result.Fun)
    fmt.Printf("cycles=%d, objective calls=%d\n", result.Nit, result.Nfev)
    fmt.Println(result.Status, result.Message)
    return nil
}

Firefly

The examples check the signed quadratic and actual evaluation counts. Python uses method="Firefly" and optimization.Firefly for the paired Hull-White fit. The calibration scalar remains root-RSS, not the unrooted sum of squares.

"""Find a negative scalar minimum with seeded, bounded firefly search."""

# itofin library
from itofin import Settings
from itofin.indexes import Euribor
from itofin.models import CalibrationErrorType, HullWhite, SwaptionHelper
from itofin.optimization import EndCriteria, Firefly
from itofin.optimize import minimize
from itofin.termstructures import FlatForward
from itofin.time import Date, DayCounter, Period


def objective(x) -> float:
    """Return a signed scalar, not a residual or its absolute value."""
    return (x[0] - 1.25) ** 2 + 4.0 * (x[1] + 0.75) ** 2 - 3.0


def calibrate_hull_white() -> None:
    """Fit one volatility parameter while keeping mean reversion fixed."""
    settings = Settings()
    settings.set_evaluation_date(Date(15, 2, 2002))
    curve = FlatForward(Date(19, 2, 2002), 0.04875825, DayCounter.actual365_fixed())
    index = Euribor.six_months(curve, settings)
    model = HullWhite(curve, 0.05, 0.01)
    helper = SwaptionHelper(
        Period(1, "Years"),
        Period(5, "Years"),
        0.1148,
        index,
        Period(1, "Years"),
        DayCounter.thirty360_bond_basis(),
        DayCounter.actual360(),
        curve,
        CalibrationErrorType.RelativePriceError,
        1.0,
    )
    method = Firefly(
        [(0.001, 0.03)],
        seed=42,
        population_size=8,
        xatol=1e-7,
        fatol=1e-10,
        maxiter=1000,
        maxfev=100000,
    )
    criteria = EndCriteria(1000, 50, 1e-6, 1e-8, 1e-8)
    model.calibrate([helper], method, criteria, True)
    result = method.last_result()
    assert result is not None
    assert result.success, result.message
    assert model.a() == 0.05
    assert 0.001 <= model.sigma() <= 0.03
    assert result.fun < 1e-6
    print(f"fixed a={model.a()}, calibrated sigma={model.sigma():.10f}")
    print(f"calibration cost={result.fun:.10g}, solver calls={result.nfev}")


def main() -> None:
    """Check the analytic optimum and report actual evaluation counts."""
    calls = 0

    def counted_objective(x) -> float:
        nonlocal calls
        calls += 1
        return objective(x)

    result = minimize(
        counted_objective,
        [-3.0, 3.0],
        method="Firefly",
        bounds=[(-4.0, 4.0), (-4.0, 4.0)],
        options={
            "seed": 42,
            "population_size": 24,
            "xatol": 1e-7,
            "fatol": 1e-10,
        },
    )
    assert result.success, result.message
    assert abs(result.x[0] - 1.25) < 1e-5
    assert abs(result.x[1] + 0.75) < 1e-5
    assert abs(result.fun + 3.0) < 1e-8
    assert result.nfev == calls
    assert result.njev == 0
    print(f"x={result.x}, scalar minimum={result.fun:.10f}")
    print(f"generations={result.nit}, objective calls={result.nfev}")
    print(result.status, result.message)
    calibrate_hull_white()


if __name__ == "__main__":
    main()
use std::convert::Infallible;

use itofin_optimize::{Bounds, Common, FireflyOptions, GlobalOptions, Method, Problem, minimize};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut calls = 0;
    let mut objective = |x: &[f64]| -> Result<f64, Infallible> {
        calls += 1;
        Ok((x[0] - 1.25).powi(2) + 4.0 * (x[1] + 0.75).powi(2) - 3.0)
    };
    let problem = Problem {
        x0: vec![-3.0, 3.0],
        bounds: Some(Bounds {
            lower: vec![-4.0, -4.0],
            upper: vec![4.0, 4.0],
        }),
    };
    let method = Method::Firefly(FireflyOptions {
        global: GlobalOptions {
            seed: 42,
            population_size: Some(24),
            xatol: Some(1e-7),
            fatol: Some(1e-10),
            ..GlobalOptions::default()
        },
        ..FireflyOptions::default()
    });
    let result = minimize(&mut objective, &problem, &method, &Common::default())?;
    assert!(result.success, "{}", result.message);
    assert!((result.x[0] - 1.25).abs() < 1e-5);
    assert!((result.x[1] + 0.75).abs() < 1e-5);
    assert!((result.fun + 3.0).abs() < 1e-8);
    assert_eq!(result.nfev, calls);
    assert_eq!(result.njev, 0);
    println!("x={:?}, scalar minimum={:.10}", result.x, result.fun);
    println!(
        "generations={}, objective calls={}",
        result.nit, result.nfev
    );
    println!("{}", result.status);
    Ok(())
}
package main

import (
    "context"
    "fmt"
    "log"
    "math"

    itofin "github.com/benbenbang/libitofin/sdk/go"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    xatol, fatol := 1e-7, 1e-10
    calls := 0
    objective := func(x []float64) (float64, error) {
        calls++
        return math.Pow(x[0]-1.25, 2) + 4*math.Pow(x[1]+0.75, 2) - 3, nil
    }
    result, err := itofin.Minimize(context.Background(), objective, []float64{-3, 3},
        itofin.Firefly{
            Bounds: [][2]float64{{-4, 4}, {-4, 4}},
            Seed:   42, PopulationSize: 24, XAtol: &xatol, FAtol: &fatol,
        })
    if err != nil {
        return err
    }
    if !result.Success || math.Abs(result.X[0]-1.25) >= 1e-5 ||
        math.Abs(result.X[1]+0.75) >= 1e-5 || math.Abs(result.Fun+3) >= 1e-8 || result.Njev != 0 || result.Nfev != calls {
        return fmt.Errorf("unexpected analytic result: %+v", result)
    }
    fmt.Printf("x=%v, scalar minimum=%.10f\n", result.X, result.Fun)
    fmt.Printf("generations=%d, objective calls=%d\n", result.Nit, result.Nfev)
    fmt.Println(result.Status, result.Message)
    return nil
}

Algorithm references

Implemented independently from Storn and Price (1997), Differential Evolution: A Simple and Efficient Heuristic for Global Optimization over Continuous Spaces. The local generator follows Steele, Lea and Flood (2014), Fast Splittable Pseudorandom Number Generators.

PSO is implemented independently from Kennedy and Eberhart (1995), Particle Swarm Optimization, with the authors' paper hosted by the University of Washington, and the inertia formulation in Shi and Eberhart (1998), A Modified Particle Swarm Optimizer. Our synchronous update, zero initial velocity, absorbing bounds and convergence policies are explicit implementation choices, not unchanged paper defaults.

Hybrid annealing uses the Metropolis acceptance rule described by Kirkpatrick, Gelatt and Vecchi (1983), Optimization by Simulated Annealing, and exploratory coordinate polling inspired by Hooke and Jeeves (1961), Direct Search Solution of Numerical and Statistical Problems. Reflected uniform proposals, geometric cooling, periodic best-point reannealing, local polling and finite-budget stopping are explicit implementation policies.

Firefly attraction and random motion are independently implemented from Yang (2009), Firefly Algorithms for Multimodal Optimization. Frozen eligibility, row-ordered sequential moves, box-scaled distance, reflection, noise decay and best-point archival are explicit local policies, not a promise of matching the paper's experiments or another library's trajectory.

No upstream optimizer implementation is copied or imported. This is not a claim of clean-room provenance or QuantLib/SciPy trajectory equivalence.