Source code for hyperelastic.lab._optimize

import matplotlib.pyplot as plt
import numpy as np

from ._curve_fit import concatenate_curve_fit


[docs] class Optimize: r"""Take lists of experiments and simulations and find material parameters for the simulation model to obtain a best possible representation of the experiments by the simulations. Attributes ---------- experiments : list A list of :class:`Experiment <.lab.Experiment>`. simulations : list A list of :class:`Simulation <.lab.Simulation>`. parameters : array_like The material parameters. mask : list of array_like A list of masks to take the optimization-relevant data points. Examples -------- Three different test specimens are subjected to displacement-controlled uniaxial, planar and biaxial tension. The applied displacement and reaction force data is used to create the :class:`Experiments <.lab.Experiment>`. The test specimen for the uniaxial load case has a cross-sectional area of :math:`A=25~\text{mm}^2` and a length of :math:`L=100~\text{mm}`. >>> area = 25 >>> length = 100 Some synthetic experimental data is generated to demonstrate the capabilities of the optimization. >>> import numpy as np >>> displacement = np.linspace(0, 2 * length, 100) >>> stretch = 1 + displacement / length >>> force = (stretch - 1 / stretch ** 2 + (stretch - 1)**5 / 10) * area With this reference experimental data at hand, the list of experiments is created. In this example, the displacement and force data as well as the cross-sectional area are scaled from the synthetic uniaxial experimental data to the other (synthetic) experiments. >>> from hyperelastic import lab >>> experiments = [ >>> lab.Experiment( >>> label="Uniaxial Tension", >>> displacement=displacement, >>> force=force, >>> area=area, >>> length=length, >>> ), >>> lab.Experiment( >>> label="Planar Tension", >>> displacement=displacement[::2], >>> force=force[::2], >>> area=area / (8 / 7), >>> length=length, >>> ), >>> lab.Experiment( >>> label="Biaxial Tension", >>> displacement=displacement[::2] / 2, >>> force=force[::2], >>> area=area / (4 / 5), >>> length=length, >>> ), >>> ] A function which takes the material parameters and returns the hyperelastic constitutive material formulation has to be provided for the simulation objects. Here, we use an isotropic invariant-based :class:`third-order deformation <.models.invariants.ThirdOrderDeformation>` material formulation. >>> def material(**kwargs): >>> "A third-order deformation material formulation." >>> >>> tod = hyperelastic.models.invariants.ThirdOrderDeformation(**kwargs) >>> framework = hyperelastic.InvariantsFramework(tod) >>> >>> return hyperelastic.DeformationSpace(framework) The list of labels of the material parameters is used for all simulation objects. >>> labels = ["C10", "C01", "C11", "C20", "C30"] Next, the simulations for all three loadcases are created. It is important to take the stretches from the according experiments. >>> simulations = [ >>> lab.Simulation( >>> loadcase=lab.Uniaxial(), >>> stretch=experiments[0].stretch, >>> material=material, >>> labels=labels, >>> ), >>> lab.Simulation( >>> loadcase=lab.Planar(), >>> stretch=experiments[1].stretch, >>> material=material, >>> labels=labels, >>> ), >>> lab.Simulation( >>> loadcase=lab.Biaxial(), >>> stretch=experiments[2].stretch, >>> material=material, >>> labels=labels, >>> ), >>> ] Both the list of experiments and the list of simulations are passed to :class:`Optimize <.lab.Optimize>`, where its curve-fit method acts as a simple wrapper for :class:`scipy.optimize.curve_fit`. The initial material parameters are all set to one. >>> optimize = lab.Optimize( >>> experiments=experiments, >>> simulations=simulations, >>> parameters=np.ones(5), >>> ) >>> parameters, pcov = optimize.curve_fit(method="lm") >>> parameters array([ 0.50430357, -0.01413309, 0.0141219 , -0.01641752, 0.00492179]) >>> fig, ax = optimize.plot(title="Third-Order Deformation") .. image:: images/fig_optimize-tod.png """ def __init__(self, experiments, simulations, parameters, mask=None): """Take lists of experiments and simulations and find material parameters for the simulation model to obtain a best possible representation of the experiments by the simulations. Parameters ---------- experiments : list of experiments A list of :class:`hyperelastic.lab.Experiment`. simulations : list of :class:`hyperelastic.lab.Simulations` A list of :class:`hyperelastic.lab.Simulations`. parameters : array_like The material parameters. mask : list of array_like or None, optional A list of masks to take the optimization-relevant data points (default is None). """ self.experiments = experiments self.simulations = simulations self.parameters = parameters self.mask = mask if self.mask is None: self.mask = [slice(None)] * len(self.experiments)
[docs] def init_curve_fit(self, *args, **kwargs): self.f = [simulation.stress_curve_fit for simulation in self.simulations] self.x = [ experiment.stretch[mask] for mask, experiment in zip(self.mask, self.experiments) ] self.y = [ experiment.stress()[mask] for mask, experiment in zip(self.mask, self.experiments) ] f0 = np.concatenate( [fi(xi, *self.parameters) for fi, xi in zip(self.f, self.x)] ) self.errors = np.ones_like(self.parameters) * np.nan self.residuals = f0 - np.concatenate(self.y) for simulation in self.simulations: simulation.parameters = self.parameters return self.parameters
[docs] def mean_relative_std(self): """Return the relative mean of the standard deviations of the material parameters, normalized by the absolute mean-values of the parameters. """ return np.mean(self.errors / np.abs(self.parameters)) * 100
[docs] def norm_residuals(self): "Return the norm of the residuals." return np.linalg.norm(self.residuals)
[docs] def curve_fit(self, *args, **kwargs): "Use non-linear least squares to fit a list of functions to a list of data." p0 = self.init_curve_fit(*args, **kwargs) popt, pcov = concatenate_curve_fit(self.f, self.x, self.y, p0, *args, **kwargs) fopt = np.concatenate([fi(xi, *popt) for fi, xi in zip(self.f, self.x)]) self.parameters[:] = popt self.errors[:] = np.sqrt(np.diag(pcov)) self.residuals[:] = fopt - np.concatenate(self.y) for simulation in self.simulations: simulation.parameters[:] = self.parameters return self.parameters, pcov
[docs] def plot(self, title=None): fig, ax = plt.subplots() for i, (simulation, experiment) in enumerate( zip(self.simulations, self.experiments) ): label = experiment.label fig, ax = simulation.plot_stress_stretch( f"C{i}", label=f"{label}", lw=3, ax=ax ) fig, ax = experiment.plot_stress_stretch( f"C{i}", label=f"{label} (Experiment)", lw=0.7, ax=ax ) ax.legend(loc=2) text = "\n".join( [ rf"{n} = {p:1.2g} $\pm$ {2*s:1.2g}" for n, p, s in zip(simulation.labels, self.parameters, self.errors) ] ) ax.text( 0.98, 0.02, text + "\n\n" + f"Absolute Norm of Residuals = {self.norm_residuals(): 1.4g}" + "\n" + f"Mean Std. Deviation of Parameters = {self.mean_relative_std(): 1.1f}%", horizontalalignment="right", verticalalignment="bottom", fontsize="small", transform=ax.transAxes, ) if title is not None: ax.set_title(title) fig.tight_layout() return fig, ax