Parameter Identification

Simcoon provides a Python-based parameter identification workflow using scipy.optimize and a generic key-based file templating system. This replaces the former built-in C++ genetic algorithm and allows users to choose any optimizer, cost function, or external simulation tool.

Overview

The identification workflow consists of three components:

  1. Key system (Parameter / Constant): generic file templating that replaces placeholders with parameter values in any input file

  2. Forward model: simcoon solver, L_eff homogenization, or any external tool (e.g., fedoo for FEMU)

  3. Optimizer: sim.identification() wraps scipy.optimize.differential_evolution, or call scipy directly for more control

Template files (keys/)          Working files (data/)
┌──────────────────┐    copy    ┌──────────────────┐
│  ... @2p ...     │ ────────>  │  ... @2p ...     │
│  ... @3p ...     │            │  ... @3p ...     │
└──────────────────┘            └──────────────────┘
                                       │ apply
                                       v
                                ┌──────────────────┐
                                │  ... 73000 ...   │  ──> Forward model
                                │  ... 0.22 ...    │      (solver, L_eff, ...)
                                └──────────────────┘

Key System

The key system decouples the optimizer from the simulation tool. Template files contain alphanumeric placeholders (keys) that are replaced at each iteration with the current parameter values.

Parameter class

from simcoon.parameter import Parameter, read_parameters, copy_parameters, apply_parameters

# Create parameters programmatically
params = [
    Parameter(number=0, bounds=(100, 300), key="@E",
              sim_input_files=["material.dat"]),
    Parameter(number=1, bounds=(0.1, 0.4), key="@nu",
              sim_input_files=["material.dat"]),
]

# Or read from a file
params = read_parameters("data/parameters.inp")

Parameter attributes:

  • number: parameter index

  • bounds: (min, max) tuple — used as optimizer bounds

  • key: placeholder string in template files (e.g., @E, @0p)

  • sim_input_files: list of files containing this key

  • value: current value (defaults to midpoint of bounds)

Parameters file format (parameters.inp):

#Number  #min     #max     #key  #number_of_files  #files
0        100      300      @E    1                  material.dat
1        0.1      0.4      @nu   1                  material.dat

Constant class

Constants are fixed values (not optimized) that also use the key system:

from simcoon.constant import Constant, read_constants, copy_constants, apply_constants

The Constant class is a NamedTuple with fields: number, key, input_values, value, sim_input_files.

File operations

# 1. Copy template files from keys/ to data/
copy_parameters(params, src_path="keys", dst_path="data")

# 2. Replace keys with current values
params[0].value = 200.0  # set by optimizer
params[1].value = 0.3
apply_parameters(params, dst_path="data")

This works with any file format — simcoon input files, FE meshes, JSON configs, etc. The key system is deliberately simple: it performs string replacement, making it compatible with any simulation tool.

identification() — Global Optimization

sim.identification() wraps scipy.optimize.differential_evolution using the bounds from your Parameter objects. After optimization, the identified values are written back to each Parameter.value.

from simcoon.identify import identification
from simcoon.parameter import Parameter

params = [
    Parameter(0, bounds=(10000, 200000), key="@Ef",
              sim_input_files=["Nellipsoids0.dat"]),
    Parameter(1, bounds=(0.01, 0.45), key="@nuf",
              sim_input_files=["Nellipsoids0.dat"]),
]

result = identification(my_cost_function, params, seed=42, disp=True)
print(f"E_f = {params[0].value:.0f}, nu_f = {params[1].value:.3f}")

Arguments:

  • cost_fn: callable f(x) -> float where x is a parameter array

  • parameters: list of Parameter (bounds used for search space)

  • **kwargs: forwarded to differential_evolution (maxiter, popsize, tol, seed, polish=True, disp, etc.)

Returns: scipy.optimize.OptimizeResult

For more control (other optimizers, constraints, custom initialization), call scipy.optimize directly — the Parameter objects provide the bounds and values you need.

calc_cost() — Multi-Level Weighted Cost

sim.calc_cost() computes a weighted cost function with three levels of weights, designed for multi-test identification:

\[C = \text{avg}\left( W^{\text{test}}_i \cdot W^{\text{resp}}_{i,k} \cdot W^{\text{pt}}_{i,k,j} \cdot (y^{\exp}_{i,k,j} - y^{\num}_{i,k,j})^2 \right)\]

Data is organized as a list of 2-D arrays, one per test, each of shape (n_points, n_responses):

from simcoon.identify import calc_cost
import numpy as np

# Two tensile tests, each with force + displacement columns
y_exp = [
    np.column_stack([force_exp_1, disp_exp_1]),  # test 1: (N, 2)
    np.column_stack([force_exp_2, disp_exp_2]),  # test 2: (N, 2)
]
y_num = [
    np.column_stack([force_num_1, disp_num_1]),
    np.column_stack([force_num_2, disp_num_2]),
]

# Simple MSE
cost = calc_cost(y_exp, y_num)

# NMSE per response (balances force in N vs disp in mm)
cost = calc_cost(y_exp, y_num, metric='nmse_per_response')

# Per-test weights (emphasize test 2)
cost = calc_cost(y_exp, y_num, w_test=np.array([1.0, 3.0]))

Weight levels

Three levels of weights are combined multiplicatively:

Level

Argument

Description

Test

w_test

ndarray (n_tests,) — weight per experiment/file. Use to emphasize certain tests over others.

Response

w_response

list of ndarray (n_responses,) — weight per response column. To balance responses of different magnitudes, use metric='nmse_per_response' instead of manual weights.

Point

w_point

list of ndarray (n_points, n_responses) — weight per data point. Use for heterogeneous confidence or to mask outliers.

Metrics

Built-in metrics (numpy only, no extra dependency):

  • "mse" — Mean Squared Error (default)

  • "nmse" — Normalized MSE (divided by variance of all experimental data)

  • "nmse_per_response" — NMSE computed independently per response column, then averaged. Each column is divided by its own sum(y_exp^2), balancing responses of different magnitudes (e.g., force in N vs displacement in mm). This is the recommended metric for multi-response identification.

  • "rmse" — Root Mean Squared Error

  • "mae" — Mean Absolute Error

With scikit-learn installed (pip install simcoon[identify]):

  • "r2" — R-squared score

  • "mean_squared_error", "mean_absolute_error" — sklearn wrappers

  • Any sklearn.metrics function that accepts sample_weight

If scikit-learn is not installed and an sklearn metric is requested, a clear error message with install instructions is shown.

Using with External Solvers

The key system works with any simulation tool. For example, with fedoo (Finite Element Model Updating — FEMU):

import subprocess
from simcoon.parameter import Parameter, copy_parameters, apply_parameters
from simcoon.identify import identification, calc_cost

params = [
    Parameter(0, bounds=(100e3, 300e3), key="@E",
              sim_input_files=["material.json"]),
]

def cost(x):
    params[0].value = x[0]
    copy_parameters(params, "keys", "data")
    apply_parameters(params, "data")

    # Run external solver
    subprocess.run(["python", "run_fedoo_simulation.py"])

    # Load results and compare
    y_num = [np.loadtxt("results/reaction_force.txt")]
    y_exp = [np.loadtxt("exp_data/reaction_force.txt")]
    return calc_cost(y_exp, y_num, metric="nmse_per_response")

result = identification(cost, params, seed=42)