API Reference: arc_scope.optim¶
The optimisation module provides parameter tuning for SCOPE simulations. It adjusts parameters that cannot be retrieved from reflectance (e.g., fluorescence quantum efficiency, soil resistance) while holding ARC-retrieved biophysical parameters fixed.
For end-to-end runner examples with SIF, thermal, and coupled energy-balance fits, see the Optimization Guide.
ParameterSpec¶
@dataclass
class ParameterSpec:
name: str
initial: float
lower: float
upper: float
optimize: bool = True
transform: str = "identity"
Specification for a single optimisable parameter.
Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | SCOPE variable name (e.g., "fqe", "rss"). |
initial |
float |
required | Starting value in physical units. |
lower |
float |
required | Lower bound in physical units. |
upper |
float |
required | Upper bound in physical units. |
optimize |
bool |
True |
Whether this parameter is optimised or held fixed. |
transform |
str |
"identity" |
Reparameterisation: "identity", "log", or "logit". |
Methods¶
to_unconstrained(value) -- Map a physical value to unconstrained optimisation space:
"identity": returns the value unchanged"log": returnslog(value)(for strictly positive parameters)"logit": normalises to [0, 1] then applies logit (for bounded parameters)
to_physical(unconstrained) -- Inverse of to_unconstrained(). Maps back to physical units with clipping to bounds.
Usage¶
from arc_scope.optim.parameters import ParameterSpec
spec = ParameterSpec("fqe", initial=0.01, lower=0.001, upper=0.1, transform="log")
# Round-trip through unconstrained space
u = spec.to_unconstrained(0.01) # -4.605...
p = spec.to_physical(u) # 0.01
ParameterSet¶
@dataclass
class ParameterSet:
specs: list[ParameterSpec] = field(default_factory=list)
Collection of parameters for SCOPE optimisation. Manages the mapping between a flat optimisation vector (unconstrained space) and named SCOPE parameters (physical units).
Properties¶
optimizable -- List of ParameterSpec where optimize=True.
fixed -- List of ParameterSpec where optimize=False.
Methods¶
to_array() -- Current values as an unconstrained 1-D numpy array (optimisable parameters only).
params = ParameterSet([
ParameterSpec("fqe", initial=0.01, lower=0.001, upper=0.1, transform="log"),
ParameterSpec("rss", initial=500, lower=10, upper=5000, transform="log"),
])
x = params.to_array() # shape (2,) in unconstrained space
from_array(values) -- Convert an unconstrained array back to a dict of named physical values. Returns all parameters (optimised + fixed).
named = params.from_array(x) # {"fqe": 0.01, "rss": 500.0}
to_torch(device="cpu", dtype="float64") -- Create a PyTorch tensor with requires_grad=True for gradient-based optimisation. Returns a tensor of shape (n_optimisable,) in unconstrained space. Requires torch.
inject_into_dataset(dataset, values=None) -- Write parameter values into an xr.Dataset. If values is None, uses the initial values from each spec. Existing variables are broadcast-updated; new variables are added on the SCOPE y/x/time grid when available, otherwise as scalars.
ds = params.inject_into_dataset(scope_dataset, {"fqe": 0.02, "rss": 300.0})
Pre-configured Parameter Sets¶
The module provides ready-to-use ParameterSet instances:
SIF_OPTIMIZATION_PARAMS¶
For SIF-focused optimisation:
SIF_OPTIMIZATION_PARAMS = ParameterSet([
ParameterSpec("fqe", initial=0.01, lower=0.001, upper=0.1, transform="log"),
])
THERMAL_OPTIMIZATION_PARAMS¶
For thermal/LST optimisation:
THERMAL_OPTIMIZATION_PARAMS = ParameterSet([
ParameterSpec("Tcu", initial=25.0, lower=-20.0, upper=60.0, transform="identity"),
ParameterSpec("Tch", initial=24.0, lower=-20.0, upper=60.0, transform="identity"),
ParameterSpec("Tsu", initial=30.0, lower=-20.0, upper=75.0, transform="identity"),
ParameterSpec("Tsh", initial=27.0, lower=-20.0, upper=75.0, transform="identity"),
])
The standalone thermal workflow uses prescribed canopy/soil temperatures.
Use ENERGY_BALANCE_OPTIMIZATION_PARAMS when fitting resistance parameters
such as rss and rbs.
ENERGY_BALANCE_OPTIMIZATION_PARAMS¶
For full energy-balance optimisation:
ENERGY_BALANCE_OPTIMIZATION_PARAMS = ParameterSet([
ParameterSpec("fqe", initial=0.01, lower=0.001, upper=0.1, transform="log"),
ParameterSpec("rss", initial=500.0, lower=10.0, upper=5000.0, transform="log"),
ParameterSpec("rbs", initial=10.0, lower=1.0, upper=100.0, transform="log"),
ParameterSpec("Cd", initial=0.2, lower=0.01, upper=1.0, transform="log"),
ParameterSpec("rwc", initial=0.5, lower=0.1, upper=1.0, transform="logit"),
])
ScopeObjective¶
class ScopeObjective:
def __init__(
self,
base_dataset: xr.Dataset,
observations: xr.Dataset,
target_variables: Sequence[str],
loss_fn: Callable | None = None,
scope_runner: Callable | None = None,
torch_scope_runner: Callable | None = None,
config: Any = None,
pixel_selector: Mapping[str, Any] | None = None,
): ...
Objective function wrapping a SCOPE forward pass. Injects parameters into the prepared dataset, runs SCOPE, and computes a scalar loss against coordinate-aligned observations.
Parameters¶
| Name | Type | Description |
|---|---|---|
base_dataset |
xr.Dataset |
The prepared SCOPE input dataset. |
observations |
xr.Dataset |
Observed data to compare against (e.g., satellite SIF or LST). |
target_variables |
Sequence[str] |
SCOPE output variable names to extract and compare. |
loss_fn |
Callable or None |
Custom loss function (predicted, observed) -> scalar. Defaults to MSE. |
scope_runner |
Callable or None |
Custom SCOPE runner. Defaults to run_scope_simulation. |
torch_scope_runner |
Callable or None |
Optional tensor-preserving runner for autograd evaluations. |
config |
Any |
Pipeline configuration for SCOPE execution. |
pixel_selector |
Mapping[str, Any] or None |
Selector for prediction dimensions not present in observations, such as {"y": y_coord, "x": x_coord}. Use {"y": {"isel": y_index}, "x": {"isel": x_index}} for positional selection. |
Predictions and observations are aligned by shared xarray coordinates. If a
prediction has dimensions that observations lack, such as (y, x, time) output
against a tower (time,) observation, pixel_selector is required. If aligned
coordinates have no overlap, evaluation raises ValueError.
Methods¶
evaluate(params) -- Evaluate the objective (numpy/scipy-compatible). Takes a dict of named parameter values in physical units. Returns a scalar loss value.
evaluate_torch(params, param_tensor, param_set) -- Evaluate with PyTorch autograd support. Converts the unconstrained optimisation tensor through ParameterSet and returns a differentiable scalar loss. Built-in pipeline optimisation sends the PyTorch parameter tensors to the tensor-preserving SCOPE runner instead of storing them in xarray.
evaluate_value_and_gradient(values, param_set) -- Evaluate the differentiable loss and return (loss, gradient) in scipy's unconstrained parameter space. With the built-in SCOPE runner and default MSE loss, this streams SCOPE tensor chunks and backpropagates each chunk loss immediately. Raises AutogradUnavailable when PyTorch is missing or the forward path detached from autograd.
AutogradUnavailable is exported from arc_scope.optim for callers that set
use_autograd_jac="required" and want to handle detached/non-differentiable
forward paths explicitly.
Optimizer Protocol¶
@runtime_checkable
class Optimizer(Protocol):
def step(self, objective: ScopeObjective, params: ParameterSet) -> ParameterSet: ...
def converged(self) -> bool: ...
Protocol that any optimiser must satisfy for interchangeable use.
ScipyOptimizer¶
class ScipyOptimizer:
def __init__(
self,
method: str = "L-BFGS-B",
max_iter: int = 100,
tol: float = 1e-6,
use_autograd_jac: bool | str = "auto",
): ...
Wrapper around scipy.optimize.minimize. It passes scipy an autograd-backed
jac callable when the objective can provide one, so L-BFGS-B does not need
finite-difference probing. The standalone optimiser default remains "auto"
for backwards compatibility with proxy objectives, but ArcScopePipeline
defaults real built-in SCOPE optimisation runs to "required" and routes
autograd evaluation through a tensor-preserving SCOPE runner.
| Parameter | Default | Description |
|---|---|---|
method |
"L-BFGS-B" |
Scipy minimisation method. |
max_iter |
100 |
Maximum iterations. |
tol |
1e-6 |
Convergence tolerance. |
use_autograd_jac |
"auto" |
"auto" uses autograd gradients when available and falls back otherwise; "required" raises if autograd is unavailable; False disables the jac hook. Use "required" for production SCOPE optimisation. |
step(objective, params) runs a full scipy minimisation from the current parameter values and updates the specs with optimised values.
converged() returns True if the last step() call reported convergence.
TorchOptimizer¶
class TorchOptimizer:
def __init__(
self,
optimizer_cls: type | None = None,
lr: float = 0.01,
max_steps: int = 100,
tol: float = 1e-6,
**optimizer_kwargs,
): ...
Wrapper around torch.optim optimisers for gradient-based optimisation. Requires torch.
| Parameter | Default | Description |
|---|---|---|
optimizer_cls |
None (defaults to Adam) |
A torch.optim.Optimizer class. |
lr |
0.01 |
Learning rate. |
max_steps |
100 |
Maximum gradient steps per call. |
tol |
1e-6 |
Convergence tolerance on loss change. |
step(objective, params) runs gradient descent from the current parameter values.
converged() returns True if the loss stopped improving within tolerance.