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 .. math:: 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 - :math:`\theta`: parameters from ``ParameterSpace``. - :math:`f_{c,e}(\theta)`: simulated response from the adapter (reads ``.xplt`` via ``xplt`` and can slice any view). - :math:`y_{c,e}`: experimental data (``ExperimentSeries``). - :math:`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 :math:`\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 ------------- .. code-block:: text 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) ----------------------------------- .. code-block:: python 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: .. code-block:: python exp_data = np.loadtxt(DATA_PATH) exp_series = ExperimentSeries(x=exp_data[:, 0], y=exp_data[:, 1]) 2) Define parameters and bounds: .. code-block:: python parameter_space = ParameterSpace(xi=2.0) parameter_space.add_parameter(name="G", theta0=0.6, bounds=(0, 10)) 3) Create template and bindings: .. code-block:: python 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}" ), ), ], ) 4) Build one or more cases: .. code-block:: python 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())}, ) 5) Configure EngineOptions and run: .. code-block:: python 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. .. image:: ../_static/monitor.gif :alt: 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()``.