Fitter¶
The Fitter class is a lightweight wrapper around optimization
backends that minimizes objective functions defined in terms of parameter dictionaries.
It provides a consistent interface for both local and global optimizers, automatic logging, and flexible callback hooks for monitoring progress.
Supported backends¶
SciPy via
fit_scipy()
Both operate on the same callable objective interface.
Basic usage¶
The minimal setup requires two things:
An objective function (any callable
f(params: dict) -> float)A dictionary of initial parameter values
Example:
from chemfit import Fitter
def objective(params):
return 2.0 * (params["x"] - 2)**2 + 3.0 * (params["y"] + 1)**2
initial_params = {"x": 0.0, "y": 0.0}
fitter = Fitter(objective_function=objective, initial_params=initial_params)
opt_params = fitter.fit_scipy()
print(opt_params) # Expected: {"x": 2.0, "y": -1.0}
The same objective can also be optimized globally using Nevergrad:
opt_params = fitter.fit_nevergrad(budget=100)
Parameters dictionary format¶
The parameter dictionary may be nested to any depth as long as all leaf values are floating-point numbers.
# Allowed
params = {
"foo": {
"bar": {"a": 1.0},
"b": 2.0,
}
}
# Not allowed
params = {
"foo": 2.0,
"bar": [1.0, 2.0], # <-- lists are not allowed
}
The Fitter automatically flattens and unflattens nested dictionaries internally using
pydictnest during optimization.
Bounds¶
Bounds can be specified for each parameter as a dictionary that mirrors the structure
of the params dictionary. Each leaf node is a tuple (lower, upper) of floats.
Example:
bounds = {
"foo": {
"bar": {"a": (0.0, 2.0)},
"b": (-1.0, 3.0),
}
}
The bounds are passed to the constructor:
fitter = Fitter(
objective_function=objective,
initial_params=initial_params,
bounds=bounds,
)
Notes:
You can omit bounds for any parameter.
If bounds are given, both lower and upper must be specified.
Bad and near-bound regions¶
To make optimization more robust, the Fitter monitors numerical issues:
Invalid or non-float returns from the objective are replaced with
value_bad_params(default:1e5) and logged.If the initial loss equals or exceeds this threshold, a warning is issued.
After fitting, if
near_bound_tolis provided, parameters that are close to their bounds trigger a log message listing the affected parameters.
These checks help detect misconfigured or unstable objective functions early.
Callback system¶
The Fitter allows you to register one or more callbacks that are executed every
n_steps of the optimization process.
Each callback receives a CallbackInfo dataclass with the fields:
opt_params: Best parameters found so faropt_loss: Best loss value found so farcur_params: Parameters of the most recent stepcur_loss: Loss value of the most recent stepstep: Step counter (not necessarily equal to number of function evaluations)info: Reference to the currentFitInfo
Example:
def print_progress(info):
print(f"Step {info.step}: loss = {info.cur_loss:.3f}")
fitter.register_callback(print_progress, n_steps=5)
opt_params = fitter.fit_scipy()
You can register multiple callbacks; they are executed in order of registration.
FitInfo structure¶
During a fit, a FitInfo instance tracks global run statistics:
initial_value: Objective value at initial parametersfinal_value: Objective value after optimizationtime_taken: Total wall-clock time in secondsn_evals: Number of objective function evaluations
This object is reset before each new fit and accessible as fitter.info.
Example:
opt_params = fitter.fit_scipy()
print(fitter.info.time_taken)
print(fitter.info.n_evals)
Backend differences¶
SciPy (fit_scipy)
Uses local gradient-based optimizers such as
L-BFGS-B.Supports bounds directly.
Suitable for smooth, differentiable objectives.
Nevergrad (fit_nevergrad)
Uses global and derivative-free optimizers.
Accepts a
budget(number of evaluations) and optimizer name (e.g.,"NgIohTuned").Useful for noisy, non-smooth, or black-box objectives.
opt_params = fitter.fit_nevergrad(budget=200, optimizer_str="CMA")
Lifecycle hooks¶
Internally, each fit runs through two hooks:
hook_pre_fit()— resets state, evaluates initial loss, starts timer.hook_post_fit()— finalizes logging, checks for near-bound parameters.
These can be overridden in subclasses if you want to extend or integrate the Fitter with custom monitoring systems.
Summary¶
Works with any callable objective:
f(params: dict) -> floatSupports nested parameter dictionaries
Unified interface for SciPy and Nevergrad backends
Logging and safety checks for invalid objective returns
Callback system for progress monitoring
Tracks timing and evaluation counts through
FitInfo
ChemFit’s Fitter provides a simple, consistent, and reliable way to drive parameter optimization in scientific workflows.