NAME

Physics::Ellipsometry::VASE::Optimizer - Global optimization algorithms for ellipsometry model fitting

SYNOPSIS

use PDL;
use Physics::Ellipsometry::VASE::Optimizer qw(differential_evolution
                                               grid_search);

# Define an objective function (e.g., MSE from a VASE model)
my $objective = sub {
    my ($params_pdl) = @_;
    # ... evaluate model, return scalar cost (chi², MSE, etc.)
};

# Differential Evolution — find global minimum
my ($best, $cost) = differential_evolution(
    objective => $objective,
    bounds    => [ [0, 500], [1, 4], [0, 1] ],
    verbose   => 1,
);
printf "DE result: cost=%.4f  params=%s\n", $cost, $best;

# Grid Search — scan a 1-D or 2-D landscape
my ($best_g, $cost_g) = grid_search(
    objective   => $objective,
    base_params => pdl([100, 2.0, 0.01]),
    grid        => [ { index => 0, min => 10, max => 300, steps => 30 } ],
);

DESCRIPTION

Levenberg-Marquardt (LM) is a local optimizer — it converges quickly but can get trapped in local minima, especially when the initial parameter guesses are far from the true values. This is a common problem in ellipsometry, where the cost surface often has multiple minima due to thin-film interference periodicity (thickness ambiguity) and correlations between dispersion parameters.

Physics::Ellipsometry::VASE::Optimizer provides global search strategies that explore the parameter space broadly and return a good starting point for subsequent LM refinement. The typical workflow is:

1. Global search  →  approximate minimum
2. LM refinement  →  precise, converged fit

FUNCTIONS

differential_evolution

my ($best_pdl, $best_cost) = differential_evolution(%args);

Differential Evolution (DE/rand/1/bin) is a population-based stochastic optimizer introduced by Storn and Price (1997). It maintains a population of candidate solutions and evolves them through mutation, crossover, and selection — similar in spirit to a genetic algorithm, but operating directly on real-valued vectors with no encoding step.

How it works:

1. Initialisation — random population within the bounds.
2. Mutation — for each member x_i, create a mutant vector:
v = x_r0 + F · (x_r1 − x_r2)

where r0, r1, r2 are distinct random population members and F is the mutation factor controlling the step size.

3. Crossover — mix the mutant with the current member using binomial crossover with probability CR.
4. Selection — keep the trial if it has lower cost.
5. Convergence — stop when the population diversity (relative spread across each dimension) falls below the tolerance, or after maxiter generations.

Arguments:

objective (required)

Code reference. Receives a PDL piddle of parameters and returns a scalar cost value (lower is better).

bounds (required)

Arrayref of [min, max] pairs, one per parameter dimension.

pop_size

Population size. Default: 30, but automatically raised to at least 5× the number of dimensions.

F

Mutation factor (0 to 2). Default: 0.7. Higher values explore more aggressively; lower values exploit locally.

CR

Crossover probability (0 to 1). Default: 0.9. Higher values mix more dimensions per trial.

maxiter

Maximum number of generations. Default: 200.

tol

Convergence tolerance on population diversity. Default: 1e-6.

seed

Optional random seed for reproducibility.

verbose

Print progress every 20 generations. Default: 0.

Returns: ($best_pdl, $best_cost).

Example — find thickness and Cauchy A for a single-layer film:

use Physics::Ellipsometry::VASE;
use Physics::Ellipsometry::VASE::Optimizer qw(differential_evolution);
use Physics::Ellipsometry::VASE::TMM qw(psi_delta);
use Physics::Ellipsometry::VASE::Dispersion qw(cauchy_nk);

my $vase = Physics::Ellipsometry::VASE->new(layers => 1);
$vase->load_data('sample.dat');

my $objective = sub {
    my ($p) = @_;
    my $d = $p->at(0);
    my $A = $p->at(1);
    my ($n, $k) = cauchy_nk($vase->{wavelength}, $A, 0.01, 0.0);
    my $N_film = $n + i() * $k;
    my ($psi, $delta) = psi_delta(
        $vase->{wavelength}, $vase->{angle},
        [$vase->{N_air}, $N_film, $vase->{N_sub}], [$d],
    );
    my $resid = ($psi - $vase->{psi_data})**2
              + ($delta - $vase->{delta_data})**2;
    return sum($resid)->sclr;
};

my ($best, $cost) = differential_evolution(
    objective => $objective,
    bounds    => [ [1, 500], [1.3, 3.0] ],   # thickness, Cauchy A
    pop_size  => 50,
    maxiter   => 300,
    verbose   => 1,
    seed      => 42,
);

printf "Best: d=%.1f nm, A=%.4f  (cost=%.4f)\n",
       $best->at(0), $best->at(1), $cost;

# Now refine with LM
$vase->set_model(sub { ... });
my $refined = $vase->fit($best);
my ($best_pdl, $best_cost) = grid_search(%args);

Grid search systematically evaluates the objective function at every point on a regularly spaced grid over one or more parameter dimensions. All parameters not included in the grid are held at their base_params values.

This is a brute-force method best suited for:

  • 1-D scans — e.g., sweeping thickness to find the correct interference order before LM refinement.

  • 2-D maps — e.g., scanning thickness + refractive index to visualise the cost landscape.

For three or more parameters the number of evaluations grows exponentially and DE is usually a better choice.

Arguments:

objective (required)

Code reference. Receives a PDL piddle of parameters and returns a scalar cost value.

base_params (required)

PDL piddle of default parameter values. Grid dimensions override their respective elements; all others stay fixed.

grid (required)

Arrayref of grid axis specifications, each a hashref:

{ index => $param_index,      # 0-based position in params PDL
  min   => $lower_value,
  max   => $upper_value,
  steps => $number_of_points }
verbose

Print the best cost at the end. Default: 0.

Returns: ($best_pdl, $best_cost).

Example — 1-D thickness scan:

my ($best, $cost) = grid_search(
    objective   => $objective,
    base_params => pdl([100.0, 2.1, 0.01]),
    grid        => [
        { index => 0, min => 10, max => 500, steps => 100 },
    ],
    verbose => 1,
);
printf "Best thickness = %.1f nm  (cost = %.4f)\n",
       $best->at(0), $cost;

Example — 2-D thickness × refractive index map:

my ($best, $cost) = grid_search(
    objective   => $objective,
    base_params => pdl([100.0, 2.1, 0.01]),
    grid        => [
        { index => 0, min => 10,  max => 500, steps => 50 },
        { index => 1, min => 1.3, max => 3.0, steps => 50 },
    ],
    verbose => 1,
);
printf "Best: d=%.1f nm, n=%.3f  (cost=%.4f)\n",
       $best->at(0), $best->at(1), $cost;

CHOOSING A STRATEGY

Few parameters (1–2), known ranges"grid_search"

Fast, deterministic, easy to visualise.

Many parameters (3+), or unknown ranges"differential_evolution"

Handles high dimensions, robust against local minima, stochastic.

Hybrid approach (recommended)

Use grid search or DE to find the basin of attraction, then pass the result to "fit" in Physics::Ellipsometry::VASE for LM refinement:

my ($coarse, $_) = differential_evolution(...);
my $precise = $vase->fit($coarse);

SEE ALSO

Physics::Ellipsometry::VASE, Physics::Ellipsometry::VASE::Parameter

R. Storn and K. Price, "Differential Evolution — A Simple and Efficient Heuristic for Global Optimization over Continuous Spaces", J. Global Optim. 11, 341 (1997).