Skip to content

Optimize

The optimization seam: parameterizations that build configs, objectives that score results, and the searches that drive them.

femforge.optimize

Optimization seam: a Parameterization turns a point into a SimulationConfig, an Objective scores its Result, and GridSearch records each point in a Study.

Modules:

  • max_current –

    Max-current search: a closed-form I² seed plus a bracketed regula-falsi search for the largest current whose steady temperature stays at or below the limit.

  • objective –

    Objectives: a typed Result reduced to a scalar to optimize, converted to SI before its units are stripped.

  • parameterization –

    The free variables: a point in parameter space -> a concrete SimulationConfig, through the knobs a study declares.

  • search –

    A naive grid search: walk the Cartesian product of the grid through the driver and record everything in a Study.

  • study –

    The Study: a queryable log of (point, result, objective, run_id) records across an optimization run.

Classes:

Parameterization pydantic-model

Bases: BaseModel

Maps a point in parameter space -> a concrete SimulationConfig.

Holds a fixed base config plus the Knobs that perturb it — the single place that encodes which knobs vary.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

base pydantic-field

The fixed config the knobs perturb; everything not driven by a knob comes from here.

knobs pydantic-field

The free variables, each writing one named scalar onto the config; any Knob subclass, from any package.

to_config

to_config(point: dict[str, float]) -> SimulationConfig

Build the SimulationConfig for one parameter-space point (knob name -> value), leaving the base untouched.

The point's keys must be exactly the knob names; a missing or unknown key raises.

Source code in src/femforge/optimize/parameterization.py
def to_config(self, point: dict[str, float]) -> SimulationConfig:
    """Build the [`SimulationConfig`][femforge.config.SimulationConfig] for one parameter-space ``point`` (knob name -> value), leaving the base untouched.

    The point's keys must be exactly the knob names; a missing or unknown key raises.
    """
    names: set[str] = {knob.name for knob in self.knobs}
    missing: set[str] = names - point.keys()
    unknown: set[str] = point.keys() - names
    if missing or unknown:
        raise ValueError(f"point keys {sorted(point)} do not match knobs {sorted(names)} (missing {sorted(missing)}, unknown {sorted(unknown)})")

    config: SimulationConfig = self.base
    for knob in self.knobs:
        config = knob.apply(config, point[knob.name])

    return config

space

space() -> dict[str, ParamSpec]

The free variables keyed by name, each with its declared bounds — the schema a search walks (or a future optimizer samples).

Source code in src/femforge/optimize/parameterization.py
def space(self) -> dict[str, ParamSpec]:
    """The free variables keyed by name, each with its declared bounds — the schema a search walks (or a future optimizer samples)."""
    return {knob.name: knob.spec() for knob in self.knobs}

ParamSpec pydantic-model

Bases: BaseModel

The declared closed [low, high] bounds of one free variable — the schema entry Parameterization.space returns, not the sample set.

Config:

  • use_attribute_docstrings: True

Fields:

low pydantic-field

low: float

Inclusive lower bound of the free variable.

high pydantic-field

high: float

Inclusive upper bound of the free variable.

Knob pydantic-model

Bases: BaseModel

Base: one free variable that maps a scalar onto a field of a SimulationConfig.

Subclasses pin a kind discriminator and implement apply; never instantiated directly.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

name pydantic-field

name: str

Parameter-space key this knob reads from a point dict (unique within a Parameterization).

low pydantic-field

low: float

Inclusive lower bound of the knob's value.

high pydantic-field

high: float

Inclusive upper bound of the knob's value.

spec

spec() -> ParamSpec

The declared range of this knob, for Parameterization.space.

Source code in src/femforge/optimize/parameterization.py
def spec(self) -> ParamSpec:
    """The declared range of this knob, for [`Parameterization.space`][]."""
    return ParamSpec(low=self.low, high=self.high)

apply

apply(config: SimulationConfig, value: float) -> SimulationConfig

Return a copy of config with this knob set to value; implementations must not mutate config (the base config is shared across every grid point). The base raises.

Source code in src/femforge/optimize/parameterization.py
def apply(self, config: SimulationConfig, value: float) -> SimulationConfig:
    """Return a copy of ``config`` with this knob set to ``value``; implementations must not mutate ``config`` (the base config is shared across every grid point). The base raises."""
    raise NotImplementedError

GlobalMeshSize pydantic-model

Bases: Knob

Knob over MeshConfig.global_mesh_size — the domain-wide target element size (in metres; the pipeline is SI).

Fields:

kind pydantic-field

kind: Literal['global_mesh_size'] = 'global_mesh_size'

The discriminator a serialized parameterization reads this knob back by.

apply

apply(config: SimulationConfig, value: float) -> SimulationConfig

A copy of config with global_mesh_size set to value.

Source code in src/femforge/optimize/parameterization.py
def apply(self, config: SimulationConfig, value: float) -> SimulationConfig:
    """A copy of ``config`` with ``global_mesh_size`` set to ``value``."""
    mesh = config.mesh.model_copy(update={"global_mesh_size": value})
    return config.model_copy(update={"mesh": mesh})

RegionMeshSize pydantic-model

Bases: Knob

Knob over one tagged region's EntityTag.mesh_size, such as a finer air gap than the bulk.

Targets the region by tag name and raises when the base config has no such region.

Fields:

kind pydantic-field

kind: Literal['region_mesh_size'] = 'region_mesh_size'

The discriminator a serialized parameterization reads this knob back by.

tag pydantic-field

tag: str

Derived EntityTag.name (e.g. "Phase0") of the region whose mesh_size this knob sets.

apply

apply(config: SimulationConfig, value: float) -> SimulationConfig

A copy of config with the mesh_size of the region named tag set to value.

Source code in src/femforge/optimize/parameterization.py
def apply(self, config: SimulationConfig, value: float) -> SimulationConfig:
    """A copy of ``config`` with the ``mesh_size`` of the region named ``tag`` set to ``value``."""
    if not any(tag.name == self.tag for tag in config.domain.tags):
        raise ValueError(f"no region tagged {self.tag!r} in the base config")
    # Replace only the matching tag; other tags are shared unchanged — apply must not mutate the base.
    tags = [tag.model_copy(update={"mesh_size": value}) if tag.name == self.tag else tag for tag in config.domain.tags]

    domain = config.domain.model_copy(update={"tags": tags})
    return config.model_copy(update={"domain": domain})

Objective

Bases: Protocol

A result → scalar contract.

NetForceMagnitude

Maximize the net force magnitude on the moving array (|force| in N).

A solve that produced no force scores -inf, so it never wins a maximization.

PhaseResistanceFromPower

PhaseResistanceFromPower(*, drive_volts: float, phases: int = 3)

The mean per-phase resistance (Ω) of a current-conduction run, from the Joule volume integral: R = phases·V²/P.

A run without a positive Joule power column scores nan.

Source code in src/femforge/optimize/objective.py
def __init__(self, *, drive_volts: float, phases: int = 3) -> None:
    """Score runs that drive all ``phases`` near-identical phases at once at ``drive_volts``."""
    self.drive_volts: float = drive_volts
    self.phases: int = phases

Study pydantic-model

Bases: BaseModel

The queryable log of an optimization run: one StudyRecord per evaluated point.

best is the argmax/argmin over the objective; to_dataframe flattens to a units-stripped table for analysis and plotting.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

records pydantic-field

records: list[StudyRecord]

Every evaluated point, in evaluation order.

record

record(*, point: dict[str, float], run_id: str, result: Result, objective: float) -> StudyRecord

Append and return a StudyRecord for one evaluated point.

Parameters:

  • point (dict[str, float]) –

    The parameter-space coordinate, by knob name.

  • run_id (str) –

    The run bundle's content-addressed id.

  • result (Result) –

    The run's typed result.

  • objective (float) –

    The run's objective scalar.

Source code in src/femforge/optimize/study.py
def record(self, *, point: dict[str, float], run_id: str, result: Result, objective: float) -> StudyRecord:
    """Append and return a [`StudyRecord`][] for one evaluated point.

    Args:
        point: The parameter-space coordinate, by knob name.
        run_id: The run bundle's content-addressed id.
        result: The run's typed result.
        objective: The run's objective scalar.
    """
    entry: StudyRecord = StudyRecord(point=point, run_id=run_id, result=result, objective=objective)
    self.records.append(entry)
    return entry

best

best(*, maximize: bool = True) -> StudyRecord

The record with the extremal objective: argmax by default, argmin when maximize=False.

Source code in src/femforge/optimize/study.py
def best(self, *, maximize: bool = True) -> StudyRecord:
    """The record with the extremal objective: argmax by default, argmin when ``maximize=False``."""
    if not self.records:
        raise ValueError("study has no records")
    chooser = max if maximize else min
    return chooser(self.records, key=lambda entry: entry.objective)

to_dataframe

to_dataframe() -> DataFrame

Flatten the records to a pandas DataFrame, the units-stripping analysis boundary.

One row per record: the knob columns, then objective, run_id, status and the result quantities as bare SI floats.

Source code in src/femforge/optimize/study.py
def to_dataframe(self) -> pd.DataFrame:
    """Flatten the records to a pandas DataFrame, the units-stripping analysis boundary.

    One row per record: the knob columns, then ``objective``, ``run_id``, ``status`` and the result quantities as bare SI floats.
    """
    import pandas as pd

    rows: list[dict[str, object]] = []
    for entry in self.records:
        row: dict[str, object] = dict(entry.point)
        row["run_id"] = entry.run_id
        row["objective"] = entry.objective
        row["status"] = entry.result.status

        force = entry.result.force
        row["force"] = force.to(U.N).magnitude.magnitude() if force is not None else None
        row["force_x"] = force.to(U.N).magnitude.x if force is not None else None
        row["force_y"] = force.to(U.N).magnitude.y if force is not None else None
        row["force_z"] = force.to(U.N).magnitude.z if force is not None else None

        torque = entry.result.torque
        row["torque"] = torque.to(U.N * U.m).magnitude.magnitude() if torque is not None else None

        flux_density_max = entry.result.flux_density_max
        row["flux_density_max"] = flux_density_max.to(U.T).magnitude if flux_density_max is not None else None

        row["residual"] = entry.result.residual
        row["wall_time"] = entry.result.wall_time
        rows.append(row)
    return pd.DataFrame(rows)

StudyRecord pydantic-model

Bases: BaseModel

One evaluated point: where in parameter space, the run it produced, the objective scalar, and the full typed result.

Config:

  • arbitrary_types_allowed: True
  • use_attribute_docstrings: True

Fields:

point pydantic-field

point: dict[str, float]

The parameter-space coordinate (knob name → value) that produced this run.

run_id pydantic-field

run_id: str

Content-addressed id of the run bundle (== run.cache_key of the evaluated config).

objective pydantic-field

objective: float

The objective scalar for this run (the quantity the search optimizes).

result pydantic-field

result: Result

The full typed result, units intact; stripped to floats only at to_dataframe.

GridSearch

GridSearch(*, parameterization: Parameterization, grid: dict[str, list[float]], objective: Objective)

Walk the full Cartesian product of grid (knob name -> sweep values) through the driver, recording one StudyRecord per point.

Parameters:

Methods:

  • run –

    Evaluate every grid point and return the populated Study.

Source code in src/femforge/optimize/search.py
def __init__(self, *, parameterization: Parameterization, grid: dict[str, list[float]], objective: Objective) -> None:
    """Sweep ``grid`` through ``parameterization``, scoring each point with ``objective``.

    Args:
        parameterization: Turns each grid point into a config.
        grid: The values to sweep, by knob name.
        objective: Scores each result.
    """
    self.parameterization: Parameterization = parameterization
    self.grid: dict[str, list[float]] = grid
    self.objective: Objective = objective

run

run(driver: DriverLike) -> Study

Evaluate every grid point and return the populated Study.

Each record keeps the run_id the driver stamped, recomputed with run.cache_key when a stand-in stamps none.

Source code in src/femforge/optimize/search.py
def run(self, driver: DriverLike) -> Study:
    """Evaluate every grid point and return the populated [`Study`][femforge.optimize.study.Study].

    Each record keeps the ``run_id`` the driver stamped, recomputed with [`run.cache_key`][femforge.run.cache_key] when a stand-in stamps none.
    """
    study: Study = Study()
    names: list[str] = list(self.grid.keys())

    for combo in itertools.product(*(self.grid[name] for name in names)):
        point: dict[str, float] = dict(zip(names, combo))
        config: SimulationConfig = self.parameterization.to_config(point)
        result: Result = driver.evaluate(config)

        run_id: str = result.run_id if result.run_id is not None else bundle_name(cache_key(config))
        study.record(
            point=point,
            run_id=run_id,
            result=result,
            objective=self.objective(result),
        )

    return study

DriverLike

Bases: Protocol

The slice of run.Driver a grid search needs: evaluate.

Methods:

  • evaluate –

    Solve config and return its result.

evaluate

evaluate(config: SimulationConfig) -> Result

Solve config and return its result.

Source code in src/femforge/optimize/search.py
def evaluate(self, config: SimulationConfig) -> Result:
    """Solve ``config`` and return its result."""
    ...