Optimization module usage

This guide explains how to assemble an optimization run. The examples/simple_biaxial_fit.py script is an illustration, but the focus here is on the structure so you can swap in your own templates, data, and adapters.

Objective (what is minimized)

Default cost: least squares over cases/experiments

\[J(\theta) = \sum_{c \in \text{cases}} \sum_{e \in \text{experiments}} w_{c,e} \, \lVert f_{c,e}(\theta) - y_{c,e} \rVert_2^2\]
  • \(\theta\): parameters from ParameterSpace.

  • \(f_{c,e}(\theta)\): simulated response from the adapter (reads .xplt via xplt and can slice any view).

  • \(y_{c,e}\): experimental data (ExperimentSeries).

  • \(w_{c,e}\): optional weights per case/experiment.

Reparameterization (OptimizerOptions.reparametrize) maps bounded parameters to an unbounded internal space for stability. Jacobians (JacobianOptions.enabled) are finite-difference approximations of \(\partial f / \partial \theta\).

Overview (roles and features)

  • ExperimentSeries: measured data (x, y) for the objective.

  • ParameterSpace: parameters, initial guesses, bounds, optional xi.

  • FebTemplate + ParameterBinding: point to a .feb template and bind parameter names to XML paths.

  • SimulationAdapter: callable that opens .xplt with xplt, slices views, and returns arrays for the objective (any view slicing works).

  • SimulationCase: groups template, experiments, adapters, grid policy, and runtime settings (subfolder, threads).

  • EngineOptions: knobs for jacobian, cleanup, runner, storage, monitor, optimizer.

  • Engine: schedules FEBio runs (parallel if requested), collects adapter outputs, and drives the optimizer.

Assembly flow

data → ExperimentSeries
parameters → ParameterSpace
template + bindings → FebTemplate/ParameterBinding
template + experiments + adapters + grid → SimulationCase(s)
options → EngineOptions
Engine(ParameterSpace, cases, options) → run() → optimized θ

Adapter structure (xplt in action)

def read_sigma_xx(xplt_path):
    xp = xplt(str(xplt_path))
    xp.readAllStates()
    stress = xp.results["stress"].domain("volume")
    sigma_xx = stress.time(":").items(9).comp("xx")
    times = np.asarray(xp._time, dtype=float)
    return times, np.asarray(sigma_xx, dtype=float)

Adapters are plain callables: slice nodesets, surfaces, enodes, or components by name, and post-process before returning arrays.

Assembling your own run (steps)

  1. Load experiment data:

exp_data = np.loadtxt(DATA_PATH)
exp_series = ExperimentSeries(x=exp_data[:, 0], y=exp_data[:, 1])
  1. Define parameters and bounds:

parameter_space = ParameterSpace(xi=2.0)
parameter_space.add_parameter(name="G", theta0=0.6, bounds=(0, 10))
  1. Create template and bindings:

template = FebTemplate(
    TEMPLATE_PATH,
    bindings=[ParameterBinding(theta_name="G", xpath=".//Material/material[@id='1']/G")],
)

# For more flexible writes, one fitted parameter can drive multiple FEB nodes.
# Example: one angle "phi" controlling mirrored fiber vectors.
import math

template = FebTemplate(
    TEMPLATE_PATH,
    bindings=[
        EvaluationBinding(
            xpath=".//Material/material[@id='1']/solid[2]//fiber/vector",
            value=lambda th: (
                f"{math.cos(th['phi']):.6f},0,{math.sin(th['phi']):.6f}"
            ),
        ),
        EvaluationBinding(
            xpath=".//Material/material[@id='1']/solid[3]//fiber/vector",
            value=lambda th: (
                f"{math.cos(th['phi']):.6f},0,{-math.sin(th['phi']):.6f}"
            ),
        ),
    ],
)
  1. Build one or more cases:

case = SimulationCase(
    template=template,
    subfolder="biaxial",
    experiments={"biax1": exp_series},
    adapters={"biax1": SimulationAdapter(read_sigma_xx)},
    omp_threads=5,
    grids={"biax1": GridPolicyOptions(policy="fixed_user", values=FIXED_GRID.tolist())},
)
  1. Configure EngineOptions and run:

options = EngineOptions(
    jacobian=JacobianOptions(enabled=True, perturbation=1e-3, parallel=True),
    cleanup=CleanupOptions(remove_previous=True, mode="all"),
    runner=RunnerOptions(jobs=PARALLEL_JOBS, command=FEBIO_COMMAND),
    storage=StorageOptions(mode="tmp", root=WORK_ROOT),
    monitor=MonitorOptions(enabled=True),
    optimizer=OptimizerOptions(
        name="least_squares",
        settings={"ftol": 1e-6, "xtol": 1e-6},
        reparametrize=True,
    ),
)

engine = Engine(parameter_space=parameter_space, cases=[case], options=options)
result = engine.run()

Capabilities (engine)

  • Multiple cases: fit several datasets simultaneously.

  • Parallel runs: RunnerOptions.jobs controls concurrency; combine with JacobianOptions.parallel for concurrent Jacobian entries.

  • Grid policies: per-case GridPolicyOptions (fixed_user or other strategies).

  • Cleanup: remove or keep old runs via CleanupOptions.

  • Storage: temporary or disk-backed run trees via StorageOptions.

  • Monitoring/logs: live monitor plus final artifacts (generated .feb, .xplt, FEBio stdout/stderr) for the optimizer’s final result.

  • Optimizers: choose backend and pass settings; enable reparameterization for bounded problems.

EngineOptions in depth

  • JacobianOptions

    • enabled: finite-difference Jacobian on/off; required for gradient-based optimizers or sensitivities.

    • perturbation: finite-difference step size (accuracy vs. noise trade-off).

    • parallel: compute Jacobian columns concurrently (each perturbation is a FEBio run).

  • CleanupOptions

    • remove_previous: delete older transient evaluation folders during the run.

    • mode: "none" keeps transient evaluation folders for debugging; "all" removes them after final artifacts are exported.

  • RunnerOptions

    • jobs: max concurrent FEBio runs (throughput vs. CPU/RAM use).

    • command: how to invoke FEBio, e.g., ("febio4", "-i"); adjust for MPI or extra flags.

  • StorageOptions

    • mode: "tmp" runs simulations in temporary storage; "disk" runs them under root.

    • root: work directory for optimization.log, results/ curve exports, and optimized_simulations/ final FEBio outputs.

  • MonitorOptions

    • enabled: toggle live monitoring/log streaming; when enabled, progress is pushed as runs finish.

  • OptimizerOptions

    • name: optimizer backend (e.g., "least_squares").

    • settings: forwarded to the optimizer (tolerances, max iterations, damping/ line search controls, etc.).

    • reparametrize: transform bounded parameters to unbounded space internally.

Monitoring and logging

  • Live monitor: enable via MonitorOptions.enabled. A lightweight service streams run status (case, iteration, parameters, objective). Point your browser to the monitor endpoint to watch progress in near real-time.

    pyFEBiOpt monitor example
  • Logs/artifacts: the work folder contains optimization.log, results/*.txt curve exports, and optimized_simulations/ with final .feb, .xplt, and FEBio log files.

  • Cleanup: CleanupOptions controls transient evaluation folders only; exported final artifacts are preserved.

Assembly checklist

  • Prepare experiment data (ExperimentSeries).

  • Define ParameterSpace with initial guesses and bounds.

  • Set up FEB template + parameter bindings.

  • Write adapters that read .xplt via xplt and return arrays.

  • Create one or more SimulationCase objects (template, experiments, adapters, grids, runtime knobs).

  • Configure EngineOptions (jacobian, cleanup, runner, storage, monitor, optimizer).

  • Instantiate Engine and call run().