In-memory Python solver
The simcoon.solver package drives the C++ material-point solver directly
from Python: the loading path is defined with Block
and StepMeca / StepThermomeca
objects, and the results come back as numpy arrays — no path.txt,
output.dat or result files involved. Since simcoon 2.0 this package is
sim.solver, JSON is the only file format it reads, and the pre-2.0 text
inputs are converted once with scripts/legacy_to_json.py.
Quick start
A uniaxial tension test on an elastic isotropic material:
import numpy as np
from simcoon import solver
step = solver.StepMeca(
control=['strain'] + ['stress'] * 5, # E11 driven, lateral stress-free
value=[0.01, 0, 0, 0, 0, 0], # targets, Voigt order [11,22,33,12,13,23]
time=1.0, ninc=100,
)
res = solver.solve(step, "ELISO", [70000., 0.3, 1.E-5], nstatev=1)
stress = res["Stress"] # Cauchy stress history, shape (6, N)
strain = res["Strain"] # strain history (logarithmic for the default rate), shape (6, N)
Results follow the fedoo DataSet conventions — components first, one
column per increment — so they interoperate directly with fedoo utilities
(e.g. fedoo.util.voigt_tensors.StressTensorList(res["Stress"])).
Available fields include Stress (Cauchy), Kirchhoff, PKII,
Strain (the strain of the objective rate: logarithmic for the default rate, Almansi
for "truesdell"), LogStrain (\(\ln\mathbf{V}\) from \(\mathbf{F}\), exact
whatever the rate), GreenLagrange, F, R,
DR ((3, 3, N)),
TangentMatrix ((6, 6, N)), Statev, Wm, Time, Temp and,
for thermomechanical runs, Q, r, Wt and the coupled tangents
dSdE, dSdT, drdE, drdT.
Loading control
controlsets each component to'strain'(kinematically driven) or'stress'(statically driven); mixed control is solved by Newton-Raphson.Block(control_type=...)selects the strain/stress measures:'small_strain'(default),'green_lagrange'(PKII control),'logarithmic'(Kirchhoff control),'biot', or the fully kinematic'F'/'gradU'(9 components of the deformation gradient).solve(corate=...)selects the objective rate for the finite-strain control types:'jaumann','green_naghdi','logarithmic','logarithmic_R'(default),'truesdell','logarithmic_F'.'logarithmic_R'transports by the exact polar rotation increment \(\Delta\mathbf{R} = \mathbf{R}_1\mathbf{R}_0^T\), for which the tangent transport is exact — including with rotated internal-variable history (plasticity at finite rotation).mode='sinusoidal'interpolates the step sinusoidally instead of linearly;mode='tabular'follows a user table passed in memory:
t = np.linspace(0.01, 1.0, 100)
e11 = 0.015 * np.sin(np.pi * t)
step = solver.StepMeca(control=['strain'] + ['zero'] * 5,
mode='tabular',
tabular=np.column_stack([t, e11]))
In a saved path the table is the one input that is not JSON: ``save_path_json``
writes it as ``<stem>_tab<k>.csv`` next to the JSON (``#`` header naming the
columns, one row per increment) and the step's ``"tabular"`` entry holds that
filename; ``load_path_json`` reads it back, comma- or whitespace-separated.
Cyclic loading repeats the steps of a block:
Block(steps=[...], ncycle=10). Tabular steps cannot be cycled (their time column is absolute); unroll the cycles into explicit steps instead.A rotation rate can be superimposed on the mixed finite-strain control types through
StepMeca(BC_w=...)(3x3 spin matrix).
Thermomechanical loading
StepThermomeca activates the coupled heat equation
(block type 2, small strain), with thermal_control set to
'temperature' (ramp to T_final), 'heat_flux' (prescribed Q) or
'convection' (0D convection with coefficient q_conv):
step = solver.StepThermomeca(control=['stress'] * 6, value=[0.] * 6,
T_final=340., ninc=50)
# ELISO thermomechanical props: rho, c_p, E, nu, alpha
res = solver.solve(step, "ELISO", [1.E-9, 1., 70000., 0.3, 1.E-5], nstatev=1)
Mechanical steps (StepMeca) also accept T_final:
the temperature then ramps as an imposed condition of the mechanical problem
(thermal expansion without the heat equation).
Tabular thermomechanical steps support the three thermal controls: with
thermal_control='temperature' the table carries a T column when
tabular_T=True (constant temperature otherwise); with 'heat_flux' the
thermal column is the prescribed flux Q; with 'convection' there is no
thermal column and q_conv applies.
For finite-element couplers, the point-wise thermomechanical UMAT batch entry
sim.umat_T(...) complements sim.umat(...); it returns
(sigma, statev, Wm, Wt, r, dSdE, dSdT, drdE, drdT).
Mean-field composites
The mean-field models (MIHEN, MIMTN, MISCN, MIPLN) take their sub-phases in memory,
as Ellipsoid or
Layer objects passed to solve(phases=...)
or sim.L_eff(..., phases=...) (see Use the solver, phases); their own props
carry only the scheme’s settings, [mp, np, n_matrix] for Mori-Tanaka (see
Constitutive model (UMAT) catalog). Every orientation
in those objects is a simcoon.Rotation: the material frame of a phase
(material_orientation) and the geometry of an inclusion or a layer
(geometry_orientation). Any of these forms is accepted and coerced:
from simcoon.solver.micromechanics import Ellipsoid, as_rotation, euler_angles
fibre = Ellipsoid(umat_name="ELISO", concentration=0.2, nstatev=1,
props=[50000., 0.3, 0.], a1=50.,
geometry_orientation=sim.Rotation.from_rotvec([0, 0, np.pi / 4]))
fibre.geometry_orientation = (45., 0., 0.) # Euler angles, degrees
fibre.geometry_orientation = {"psi": 45., "theta": 0., "phi": 0.} # the JSON form
euler_angles(fibre.geometry_orientation) # {'psi': 45.0, 'theta': 0.0, 'phi': 0.0}
The Euler angles are the 'zxz' sequence the C++ side reads, in degrees:
as_rotation((psi, theta, phi)) is
Rotation.from_euler('zxz', [psi, theta, phi], degrees=True) (scipy’s extrinsic
zxz), applied actively. A phase at that orientation responds with
R.apply_stiffness(L_local), which the test suite pins against the solver and
L_eff. The JSON files and the dicts handed to the extension keep the angles;
euler_angles writes them back, with the usual caveat that the decomposition is not
unique at theta = 0 (the z angles merge into psi). solve(orientation=...)
and sim.L_eff(umat_name, props, nstatev, orientation=..., phases=...) take the
same forms for the frame of the material or of the whole RVE; L_eff also takes the
phase objects directly.
An orientation distribution splits one phase into phases rotated about a direction, with concentrations following the ODF:
from simcoon.solver.micromechanics import Peak, discretize_odf
peaks = [Peak(method=3, mean=90., s_dev=10.)] # Gaussian, degrees
phases = discretize_odf([matrix, fibre], num_phase=1, peaks=peaks, nphases=18,
axis=(0., 0., 1.), angle_range=(0., 180.))
L = sim.L_eff("MIMTN", props, nstatev, phases=phases)
The k-th copy is rotated by alpha_k about axis on top of the orientations the
phase already has (Rotation.from_rotvec(alpha_k * axis) * orientation), the
material frame following unless rotate_material=False; alpha_k runs from
angle_min in nphases equal steps and each copy takes the Simpson integral of
the density over its step, normalised to the parent’s concentration. The density is a
director distribution, periodic over 180 degrees: the default half turn is right when
a half turn about axis maps the inclusion onto itself (axis along or normal to
a principal axis of the inclusion); about any other direction give
angle_range=(0., 360.). The pre-2.0 Euler sweeps are the cases axis=(0, 0, 1)
(psi or phi) and axis=(1, 0, 0) (theta) from an unrotated phase. The density itself
is sim.get_densities_ODF(x, peaks), the sum of the Peak
profiles (method: 1 standard-deviation kernel, 2 hard cut-off, 3 Gaussian,
4 Lorentzian, 5 pseudo-Voigt, 6 Pearson VII, 7 uniform; formulas in the class), useful to
plot a distribution before discretising it; Peak.density(x) without periodic is
the plain profile, for a distribution of a parameter rather than of an orientation.
JSON configuration
Materials and loading paths round-trip through JSON
(save_material_json(),
save_path_json(),
load_simulation_json()):
solver.save_material_json("material.json", "ELISO", [70000., 0.3, 1.E-5], 1)
solver.save_path_json("path.json", [solver.Block(steps=[step])], T_init=293.15)
res = solver.solve(**solver.load_simulation_json("material.json", "path.json"))
Results can be persisted with res.save("run.npz") /
SolverResults.load("run.npz"), or flattened with res.to_dataframe().
Legacy text inputs
JSON is the only format simcoon reads or writes since 2.0: nothing in the package
parses path.txt, material.dat, tab_file_<n>.txt or N<kind><n>.dat.
A pre-2.0 data directory is converted once with the migration script shipped in
the repository (not in the package), the tables its mode-3 steps referenced being
rewritten as <stem>_tab<k>.csv files next to the path JSON:
python scripts/legacy_to_json.py data # writes path.json, material.json, ellipsoids<N>.json, ...
res = solver.solve(**solver.load_simulation_json("data/material.json", "data/path.json"))
Solver parameters
solve() exposes the numeric controls of the adaptive Newton loop as
keyword arguments: precision (default 1e-6), maxiter/miniter,
div_tnew_dt/mul_tnew_dt (time-step cut/growth factors), inforce,
lambda_solver (penalty stiffness of strain-driven components), plus
tangent_mode ('none', 'continuum', 'algorithmic' — default)
and solver_type.
Constitutive laws written in Python
umat_name also accepts a law object implementing simcoon.PythonUMAT
(numpy, PyTorch, …): it is registered under the PYEXT name for the duration
of the call and integrated by the C++ solver like a built-in kernel; props and
nstatev default to the object’s attributes. See Constitutive laws in Python (PYEXT).
API reference
- class simcoon.solver.StepMeca(control: str | Sequence[str | int] = 'strain', value: Sequence[float] | None = None, time: float = 1.0, ninc: int = 100, mode: str | int = 'linear', Dn_init: float = 1.0, Dn_mini: float = 0.001, BC_w: Sequence[Sequence[float]] | None = None, T_final: float | None = None, tabular: ndarray | None = None, tabular_T: bool = False)
A mechanical loading step.
- Parameters:
control (str or sequence of str) – Per-component control: ‘strain’ (kinematic) or ‘stress’ (static); ‘zero’ holds a component at zero for tabular steps. A single string applies to all components.
value (array-like, optional) – Absolute target values at the end of the step, per component, in Voigt order (6 components; 9 row-major for control types ‘F’/’gradU’). Not used for tabular steps.
time (float) – Duration of the step.
ninc (int) – Number of increments (linear and sinusoidal modes).
mode (str or int) – ‘linear’, ‘sinusoidal’ or ‘tabular’.
Dn_init (float, optional) – Initial and minimal sub-increment fraction of one increment (adaptive stepping). Defaults: 1.0 and 1e-3.
Dn_mini (float, optional) – Initial and minimal sub-increment fraction of one increment (adaptive stepping). Defaults: 1.0 and 1e-3.
BC_w (array-like, optional) – 3x3 spin matrix (rotation rate) applied during the step, for the mixed finite-strain control types (‘green_lagrange’, ‘logarithmic’, ‘biot’).
T_final (float, optional) – Target temperature at the end of the step (temperature ramp applied to the mechanical UMAT). None (default) holds the temperature.
tabular (ndarray, optional) – Mode-3 table, one row per increment. Columns: [time, (thermal column, see tabular_T and StepThermomeca.thermal_control), controlled components in Voigt order]. Required when mode=’tabular’. The time column is ABSOLUTE simulation time and must continue from the previous step’s end time (a table restarting at 0 after an earlier step is rejected: it would produce negative time increments). Tabular steps cannot be cycled (Block.ncycle must be 1).
tabular_T (bool) – Whether the tabular table contains a temperature column (after the time column). Default False (constant temperature). For thermomechanical heat-flux steps the thermal column is the flux Q and is always required, regardless of this flag.
- T_end(T_hold: float) float
Temperature at the end of the step (for chaining T_final=None steps).
- to_dict(control_type: int, T_hold: float) dict
Marshal to the dict consumed by simcoon._core.solver_run.
- Parameters:
control_type (int) – The control type of the enclosing block (drives the 6/9 sizing).
T_hold (float) – Temperature to hold when T_final is None (running block value).
- class simcoon.solver.StepThermomeca(control: str | Sequence[str | int] = 'strain', value: Sequence[float] | None = None, time: float = 1.0, ninc: int = 100, mode: str | int = 'linear', Dn_init: float = 1.0, Dn_mini: float = 0.001, BC_w: Sequence[Sequence[float]] | None = None, T_final: float | None = None, tabular: ndarray | None = None, tabular_T: bool = False, thermal_control: str = 'temperature', Q: float = 0.0, q_conv: float = 0.0)
A thermomechanical loading step (coupled heat equation).
In addition to the mechanical control of StepMeca:
- Parameters:
thermal_control (str) – ‘temperature’ (ramp to T_final), ‘heat_flux’ (prescribed flux Q) or ‘convection’ (0D convection Q = -q_conv (T - T_init)).
Q (float) – Prescribed heat flux (thermal_control=’heat_flux’).
q_conv (float) – Convection coefficient rho*c_p/tau (thermal_control=’convection’).
- T_end(T_hold: float) float
Temperature at the end of the step; flux/convection steps leave the chained hold temperature unchanged (the reached T is solution-dependent).
- class simcoon.solver.Block(steps: ~typing.List[~simcoon.solver.blocks.StepMeca] = <factory>, control_type: str | int = 'small_strain', ncycle: int = 1)
A loading block: a sequence of steps repeated ncycle times.
- Parameters:
steps (list of StepMeca / StepThermomeca) – The loading steps. If any step is a StepThermomeca, the block is thermomechanical (coupled heat equation) and all steps must be.
control_type (str or int) – Loading control (see CONTROL_TYPES). Thermomechanical blocks only support ‘small_strain’. A run is either all small strain or all finite strain: a finite-strain block restarts from F = I after a small-strain one.
ncycle (int) – Number of repetitions of the step sequence.
- to_dict(T_hold: float) dict
Marshal to the dict consumed by simcoon._core.solver_run.
- simcoon.solver.solve(blocks: Block | StepMeca | Sequence[Block | StepMeca], umat_name: str | Any | Callable, props: Sequence[float] | None = None, nstatev: int | None = None, T_init: float = 293.15, corate: str | int | None = None, tangent_mode: str | int = 2, solver_type: int = 0, orientation: Sequence[float] = (0.0, 0.0, 0.0), phases: Sequence[Any] | None = None, record_tangent: bool = True, raise_on_abort: bool = True, **params) SolverResults
Solve a homogeneous loading path with the C++ simcoon solver, in memory.
- Parameters:
blocks (Block, StepMeca or sequence of them) – The loading path. Bare steps are wrapped in a small-strain Block.
umat_name (str, PythonUMAT or callable) – Constitutive model: either the name of a built-in model (5 characters, e.g. ‘ELISO’, ‘EPICP’, ‘MODUL’) or a constitutive law written in Python (a
simcoon.PythonUMATinstance, or any callable with itsintegratekeyword signature). A Python law is registered under thePYEXTname for the duration of the call and integrated by the C++ solver exactly like a built-in kernel.props (array-like, optional) – Material properties. Required for a built-in model; defaults to the
propsattribute of a Python law.nstatev (int, optional) – Number of internal state variables. Required for a built-in model; defaults to the
nstatevattribute of a Python law.T_init (float) – Initial temperature.
corate (str, int or None) – Objective rate for the finite-strain control types (see CORATE_TYPES). Default (None): ‘logarithmic_R’ — the exact polar rotation, whose frame increment DR = R1 R0^T makes the tangent transport exact even with rotated internal-variable history (the XBM ‘logarithmic’ rate keeps a small tangent residual there). MODUL additionally requires log_R under NLGEOM (the modular Hencky composition).
tangent_mode (str or int) – Tangent operator mode: ‘none’, ‘continuum’, ‘algorithmic’ (default) or ‘closest_point’ (reserved).
solver_type (int) – 0 = classic Newton-Raphson (default), 1 = RNL (control_type 1 only).
orientation (simcoon.Rotation, dict or sequence of 3 floats) – Orientation of the material frame: a
Rotation, or its Euler angles(psi, theta, phi)in degrees (seeas_rotation()); applied actively, material frame to global frame.phases (sequence, optional) – Sub-phases of a mean-field model (MIMTN, MISCN, MIHEN, MIPLN): the Ellipsoid / Layer objects of
simcoon.solver.micromechanics, or the dicts they convert to. Leave it None for every single-phase model.record_tangent (bool) – Capture the tangent operator history (‘TangentMatrix’ or the coupled thermomechanical tangents).
raise_on_abort (bool) – Raise a RuntimeError when the solver aborts early (status != 0) instead of returning the partial history. The solver aborts when the Newton loop does not converge at the minimal increment, or when the increment falls below
Dn_miniwithinforce=0.**params – Numeric solver controls forwarded to the C++ loop: div_tnew_dt, mul_tnew_dt, miniter, maxiter, inforce, precision, lambda_solver (penalty stiffness of the strain-driven components).
- Returns:
History of the converged increments (fedoo-style data layout).
- Return type:
- class simcoon.solver.SolverResults(raw: Dict[str, ndarray], finite_blocks=None)
Dict-like access to the solver history.
- scalar_data
Scalar histories, shape (N,): ‘Time’, ‘Temp’, ‘Block’, ‘Cycle’, ‘Step’, ‘Inc’; thermomechanical runs add ‘Q’ (heat flux) and ‘r’ (heat source).
- Type:
dict
- field_data
Tensor histories, components-first: ‘Stress’ (Cauchy, (6, N)), ‘Kirchhoff’, ‘PKII’, ‘Strain’ (the strain integrated with the objective rate, (6, N): ln V for the logarithmic rates, the Almansi strain for ‘truesdell’), ‘LogStrain’ (ln V computed from F whatever the rate; the small strain on small-strain blocks), ‘GreenLagrange’ ((6, N), computed from F), ‘Statev’ ((nstatev, N)), ‘Wm’ ((4, N)), ‘F’, ‘R’, ‘DR’ ((3, 3, N)); ‘TangentMatrix’ ((6, 6, N)) for mechanical runs; thermomechanical runs add ‘Wt’ ((3, N)) and the coupled tangents ‘dSdE’ ((6, 6, N)), ‘dSdT’ ((6, N)), ‘drdE’ ((6, N)), ‘drdT’ ((N,)).
- Type:
dict
- status
0 if the simulation ran to completion, 1 on early abort (the recorded history is then partial).
- Type:
int
- get_data(key: str) ndarray
fedoo-style accessor (alias of __getitem__).
- classmethod load(filename: str) SolverResults
Load a SolverResults previously written by save().
- save(filename: str) None
Save all histories to a compressed npz archive.
- to_dataframe()
Flatten scalar and 6-component histories to a pandas DataFrame.