Problem#
OptimizationProblem bundles three separate concerns: a ParameterSpace (the input domain), a MetricSpace (the declared outputs), and an evaluation backend (typically an EvaluationPipeline, see Evaluation Pipeline).
Problem is that combination without any optimizer policy: no objective/constraint direction beyond what MetricSpace declares, no bad_metrics substitution, no callbacks.
It is directly usable wherever a sampler or a surrogate model needs declared outputs for a named assignment, without running an optimization.
Declaring metrics#
A MetricSpace collects named output declarations.
Unlike OptimizationProblem.add_objective, which derives a name from the callable when none is given, MetricSpace.add_metric always takes an explicit name.
from CADETProcess.metric_space import Metric, MetricSpace
metric_space = MetricSpace()
metric_space.add_metric(Metric('volume'))
Metric(name='volume')
Wiring a backend#
The parameter space and the evaluation backend are built the same way as standalone use of the evaluation pipeline.
import math
from dataclasses import dataclass
from CADETProcess.parameter_space import ParameterSpace, RangedParameter
from CADETProcess.evaluation_pipeline import EvaluationPipeline
@dataclass
class Column:
length: float = 0.5
diameter: float = 0.05
column = Column()
space = ParameterSpace()
space.add_evaluation_object(column)
space.add_parameter(RangedParameter('length', float, lb=0.1, ub=1.0), path='length')
space.add_parameter(RangedParameter('diameter', float, lb=0.01, ub=0.2), path='diameter')
pipeline = EvaluationPipeline(space)
pipeline.add_evaluator(
lambda col: col.length * (col.diameter / 2) ** 2 * math.pi,
output_name='volume',
)
Problem combines the three: the parameter space, the metric space, and the backend that computes the declared metrics.
from CADETProcess.problem import Problem
problem = Problem(
parameter_space=space, metric_space=metric_space, backend=pipeline, name='column_volume',
)
problem.evaluate({'length': 0.5, 'diameter': 0.05})
{'volume': array(0.00098175)}
evaluate validates the backend’s result against each declared metric’s shape and drops anything the metric space did not declare, so pipeline intermediates never leak out.
Sampling and batch evaluation#
The batch entry point, evaluate_batch(), is the same method optimizers and surrogate trainers dispatch through; used directly, it turns a set of sampled points into evaluated metrics without any optimizer in between.
Draw the samples from the parameter space (see Parameter Spaces and Sampling for the available sampler backends), then evaluate them as a batch.
from CADETProcess.parameter_space import LatinHypercubeSampler
samples = LatinHypercubeSampler().sample(space, 8, seed=0)
results = problem.evaluate_batch(samples)
results[:2]
[{'volume': array(0.00365188)}, {'volume': array(0.00511121)}]
Building a Population#
from_records() turns paired samples and results into a Population, without ever running an optimizer.
Parameter and metric values are stored as named columns, X and metrics, rather than only flat arrays.
from CADETProcess.optimization import Population
records = [{'X': sample, 'metrics': result} for sample, result in zip(samples, results)]
population = Population.from_records(records, metric_space=metric_space, parameter_space=space)
population.X['length'][:5]
array([0.47834181, 0.99539048, 0.6835071 , 0.59425348, 0.15134219])
Indexing a Population with an integer returns an IndividualView, a row lens exposing the same X/metrics accessors scoped to that one row.
population[0].X
{'length': np.float64(0.47834181017633637),
'diameter': np.float64(0.09859256554810808)}
population[0].metrics
{'volume': np.float64(0.0036518805169675414)}
A single sample goes through from_sample() instead, without wrapping it in a list.
one = Population.from_sample(
samples[0], results[0], metric_space=metric_space, parameter_space=space,
)
one[0].metrics
{'volume': np.float64(0.0036518805169675414)}
The Optimizer documents the further accessors a Population exposes on optimization results, including the flat f/g projections, the constraint-violation matrices, and the feasibility filters.
Note
Metric names on a bare MetricSpace are always explicit, unlike OptimizationProblem.add_objective/add_evaluator, which derive a default name from the callable and sanitize it into a valid Python identifier (a bare lambda becomes _lambda_).
A metric declared with an evaluation_object dimension carries its object name as a label suffix even when only one object is registered, since the dimension is part of the metric’s shape, not a special case for multiple objects.