Source code for felupe.mechanics._curve

# -*- coding: utf-8 -*-
"""
This file is part of FElupe.

FElupe is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

FElupe is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with FElupe.  If not, see <http://www.gnu.org/licenses/>.
"""

import numpy as np

from ..tools import force
from ._job import Job


[docs] class CharacteristicCurve(Job): r"""A job with a list of steps and a method to evaluate them. Force-displacement curve data is tracked during evaluation for a given :class:`~felupe.Boundary` by a built-in ``callback``. Parameters ---------- steps : list of Step A list with steps, where each step subsequently depends on the solution of the previous step. items : list of SolidBody, SolidBodyNearlyIncompressible, SolidBodyPressure, SolidBodyGravity, PointLoad, MultiPointConstraint, MultiPointContact or None, optional A list of items with methods for the assembly of sparse vectors/matrices which are used to evaluate the sum of reaction forces. If None, the total reaction forces from the :class:`~felupe.tools.NewtonResult` of the substep are used. callback : callable, optional A callable which is called after each completed substep. Function signature must be ``lambda stepnumber, substepnumber, substep, **kwargs: None``, where ``substep`` is an instance of :class:`~felupe.tools.NewtonResult`. The field container of the completed substep is available as ``substep.x``. Default is ``callback=lambda stepnumber, substepnumber, substep, **kwargs: None``. **kwargs : dict, optional Optional keyword-arguments for the ``callback`` function. Examples -------- .. pyvista-plot:: >>> import felupe as fem >>> >>> mesh = fem.Cube(n=6) >>> region = fem.RegionHexahedron(mesh) >>> field = fem.FieldContainer([fem.Field(region, dim=3)]) >>> >>> boundaries = dict() >>> boundaries["fixed"] = fem.Boundary(field[0], fx=0, skip=(False, False, False)) >>> boundaries["clamped"] = fem.Boundary(field[0], fx=1, skip=(True, False, False)) >>> boundaries["move"] = fem.Boundary(field[0], fx=1, skip=(False, True, True)) >>> >>> umat = fem.NeoHooke(mu=1, bulk=2) >>> solid = fem.SolidBody(umat, field) >>> >>> move = fem.math.linsteps([0, 1], num=5) >>> step = fem.Step(items=[solid], ramp={boundaries["move"]: move}, boundaries=boundaries) >>> >>> job = fem.CharacteristicCurve(steps=[step], boundary=boundaries["move"]).evaluate() >>> fig, ax = job.plot( ... xlabel=r"Displacement $u_1$ in mm $\rightarrow$", ... ylabel=r"Normal Force in $F_1$ in N $\rightarrow$", ... marker="o", ... ) >>> solid.plot("Principal Values of Cauchy Stress").show() See Also -------- Step : A Step with multiple substeps, subsequently depending on the solution of the previous substep. Job : A job with a list of steps and a method to evaluate them. tools.NewtonResult : A data class which represents the result found by Newton's method. """ def __init__( self, steps, boundary, items=None, callback=lambda stepnumber, substepnumber, substep, **kwargs: None, **kwargs, ): super().__init__(steps, self._callback, **kwargs) self.items = items self.boundary = boundary self.x = [] self.y = [] self.res = None self._cb = callback def _callback(self, stepnumber, substepnumber, substep, **kwargs): if self.items is not None: fun = sum([item.results.force for item in self.items]) else: fun = substep.fun self.x.append(substep.x[0].values[self.boundary.points[0]]) self.y.append(force(substep.x, fun, self.boundary)) self.res = substep self._cb(stepnumber, substepnumber, substep, **kwargs)
[docs] def plot( self, x=None, y=None, xaxis=0, yaxis=0, xlabel=None, ylabel=None, xscale=1.0, yscale=1.0, xoffset=0.0, yoffset=0.0, gradient=False, swapaxes=False, ax=None, items=None, **kwargs, ): """Plot force-displacement characteristic curves on a pre-evaluated job, tracked on a given :class:`~felupe.Boundary`. Parameters ---------- x : list of ndarray or None, optional A list with arrays of displacement data. If None, the displacement is taken from the first field of the field container from each completed substep. The displacement data is then taken from the first point of the tracked :class:`~felupe.Boundary`. Default is None. y : list of ndarray or None, optional A list with arrays of reaction force data. If None, the force is taken from the :class:`~felupe.tools.NewtonResult` of each completed substep. Default is None. xaxis : int, optional The axis for the displacement data (default is 0). yaxis : int, optional The axis for the reaction force data (default is 0). xlabel : str or None, optional The label of the x-axis (default is None). ylabel : str or None, optional The label of the y-axis (default is None). xscale : float, optional A scaling factor for the displacement data (default is 1.0). yscale : float, optional A scaling factor the reaction force data (default is 1.0). xoffset : float, optional An offset for the displacement data (default is 0.0). yoffset : float, optional An offset for the reaction force data (default is 0.0). gradient : bool, optional A flag to plot the gradient of the y-data. Uses ``numpy.gradient(edge_order=2)``. The gradient data is set to ``np.nan`` for absolute values greater than the mean value plus two times the standard deviation. Default is False. swapaxes : bool, optional A flag to flip the plot (x, y) to (y, x). Also changes the labels. ax : matplotlib.axes.Axes An axes object where the plot is placed in. items : slice, ndarray or None Indices or a range of data points to plot. If None, all data points are plotted (default is None). **kwargs : dict Additional keyword arguments for plotting in ``ax.plot(**kwags)``. Returns ------- fig : matplotlib.figure.Figure The figure object where the plot is placed in. ax : matplotlib.axes.Axes The axes object where the plot is placed in. """ if self.res is None: raise ValueError( "Results are empty. Run `job.evaluate()` and call `job.plot()` again." ) import matplotlib.pyplot as plt if x is None: x = self.x if y is None: y = self.y if items is None: items = slice(None) x = np.array(x)[items] y = np.array(y)[items] if gradient: y = np.gradient(y, x[:, xaxis], edge_order=2, axis=0) z = np.gradient(y, x[:, xaxis], edge_order=2, axis=0) cuttoff = np.mean(abs(z[:, yaxis])) + 2 * np.std(abs(z[:, yaxis])) y[abs(z) > cuttoff] = np.nan if ax is None: fig, ax = plt.subplots() else: fig = ax.get_figure() if swapaxes: x, y = y, x xlabel, ylabel = ylabel, xlabel xaxis, yaxis = yaxis, xaxis xscale, yscale = yscale, xscale ax.plot( xoffset + x[:, xaxis] * xscale, yoffset + y[:, yaxis] * yscale, **kwargs ) if xlabel is not None: ax.set_xlabel(xlabel) if ylabel is not None: ax.set_ylabel(ylabel) return fig, ax