diff --git a/python/tamaas/nonlinear_solvers/__init__.py b/python/tamaas/nonlinear_solvers/__init__.py index b96b06c..a23627c 100644 --- a/python/tamaas/nonlinear_solvers/__init__.py +++ b/python/tamaas/nonlinear_solvers/__init__.py @@ -1,221 +1,221 @@ # -*- mode:python; coding: utf-8 -*- # @file # @section LICENSE # # Copyright (©) 2016-2020 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """ Pulling solvers to nonlinear_solvers module """ from .. import EPSolver import numpy as np from scipy.sparse.linalg import LinearOperator, lgmres from scipy.linalg import norm from scipy.optimize import newton_krylov, root try: from .akantu_solver import * # noqa except ImportError: pass __all__ = ['NLNoConvergence', 'DFSANESolver', 'NewtonKrylovSolver', 'ToleranceManager'] class NLNoConvergence(Exception): """Convergence not reached exception""" pass class NewtonRaphsonSolver(EPSolver): """ Newton-Raphson based on CTO """ def __init__(self, residual, model): super(NewtonRaphsonSolver, self).__init__(residual) self.model = model self._residual = self.getResidual() self._x = self.getStrainIncrement() self.last_solution = np.zeros_like(self._x) def applyTangent(self, _input, strain): _input = np.reshape(_input, strain.shape) out = np.zeros_like(_input) self._residual.applyTangent(out, _input, strain) return out def solve(self): self._x[...] = 0 # For initial guess, compute the strain due to boundary tractions self._residual.computeResidual(self._x) self._x[...] = self._residual.getVector() res_vec = np.ravel(self._residual.getVector()) initial_norm = norm(res_vec) self._residual.computeResidual(self._x) n_max = 100 i = 0 while norm(res_vec) / initial_norm > 1e-10 and i < n_max: # Making linear tangent tangent = LinearOperator( (self._x.size, self._x.size), matvec=lambda x: self.applyTangent(x, self._x)) res_vec *= -1 correction, ok = lgmres(tangent, res_vec, tol=1e-12, maxiter=100) if ok != 0: raise Exception("LGMRES not converged") self._x += np.reshape(correction, self._x.shape) self._residual.computeResidual(self._x) i += 1 print("Solved Newton-Raphson in {} iterations".format(i)) # Computing the strain correction self.last_solution *= -1 self.last_solution += self._x # Computing displacements self._residual.computeResidualDisplacement(self.last_solution) self.last_solution[...] = self._x[...] class ScipySolver(EPSolver): """ Base class for solvers wrapping SciPy routines """ def __init__(self, residual, model, callback=None): super(ScipySolver, self).__init__(residual) self.model = model self.callback = callback self._x = self.getStrainIncrement() self._residual = self.getResidual() self.options = {'ftol': 0, 'fatol': 1e-9} @property def tolerance(self): return self.options['fatol'] @tolerance.setter - def set_tolerance(self, v): + def tolerance(self, v): self.options['fatol'] = v def solve(self): """ Solve the nonlinear plasticity equation using the scipy_solve routine """ # For initial guess, compute the strain due to boundary tractions # self._residual.computeResidual(self._x) # self._x[...] = self._residual.getVector() # Scipy root callback def compute_residual(x): self._residual.computeResidual(x) return self._residual.getVector().copy() # Solve self._x[...] = self.scipy_solve(compute_residual) # Computing displacements self._residual.computeResidualDisplacement(self._x) def reset(self): self._x[...] = 0 class NewtonKrylovSolver(ScipySolver): """ Solve using a finite-difference Newton-Krylov method """ def __init__(self, residual, model, callback=None): ScipySolver.__init__(self, residual, model, callback) def scipy_solve(self, compute_residual): try: return newton_krylov(compute_residual, self._x, f_tol=self.options['fatol'], verbose=True, callback=self.callback) except Exception as e: raise NLNoConvergence(e.what()) class DFSANESolver(ScipySolver): """ Solve using a spectral residual jacobianless method """ def __init__(self, residual, model, callback=None): ScipySolver.__init__(self, residual, model, callback) def scipy_solve(self, compute_residual): solution = root(compute_residual, self._x, method='df-sane', options=self.options, callback=self.callback) print("DF-SANE: {} ({} iterations, {})".format( solution.message, solution.nit, self.options)) if not solution.success: raise NLNoConvergence("DF-SANE did not converge") return solution.x.copy() class ToleranceManager: """ Decorator to manage tolerance of non-linear solver """ def __init__(self, start, end, rate, key='fatol'): self.start = start / rate # just anticipating first multiplication self.end = end self.rate = rate self.key = key def __call__(self, cls): orig_init = cls.__init__ orig_solve = cls.scipy_solve orig_updateState = cls.updateState def __init__(obj, *args, **kwargs): orig_init(obj, *args, **kwargs) obj.options[self.key] = self.start def scipy_solve(obj, *args, **kwargs): ftol = obj.options[self.key] ftol *= self.rate obj.options[self.key] = max(ftol, self.end) return orig_solve(obj, *args, **kwargs) def updateState(obj, *args, **kwargs): obj.options[self.key] = self.start return orig_updateState(obj, *args, **kwargs) cls.__init__ = __init__ cls.scipy_solve = scipy_solve cls.updateState = updateState return cls diff --git a/tests/conftest.py b/tests/conftest.py index d098de0..c83ff62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,234 +1,238 @@ # -*- coding: utf-8 -*- # @file # @section LICENSE # # Copyright (©) 2016-2020 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from __future__ import division, print_function import numpy as np import tamaas as tm import pytest from numpy.linalg import norm def profile(func, size, mode, amplitude): x = np.linspace(0, 1, size[0], endpoint=False, dtype=tm.dtype) y = np.linspace(0, 1, size[1], endpoint=False, dtype=tm.dtype) x, y = np.meshgrid(x, y, indexing='ij') surface = amplitude * func(2*np.pi*x*mode[0]) * func(2*np.pi*y*mode[1]) return surface.copy() @pytest.fixture(scope="session") def tamaas_fixture(): tm.initialize() yield None tm.finalize() class HertzFixture: def __init__(self, n, load): self.domain_size = 1 self.n = n self.load = load self.curvature = 0.1 self.radius = 1. / self.curvature self.e_star = 1. self.a = (3 * load / (4 * self.curvature * self.e_star))**(1. / 3.) self.x = np.linspace(-self.domain_size / 2., self.domain_size / 2., self.n, dtype=tm.dtype) self.y = self.x.copy() self.x, self.y = np.meshgrid(self.x, self.y) self._computeSurface() self._computePressure() self._computeDisplacement() def _computeDisplacement(self): r = np.sqrt(self.x**2 + self.y**2) self.displacement = np.zeros_like(r) contact = r < self.a self.displacement[contact] = self.surface[contact] self.displacement[~contact] = \ (self.surface[~contact] + self.a / (np.pi * self.radius) * np.sqrt(r[~contact]**2 - self.a**2) + (r[~contact]**2 - 2 * self.a**2) / (np.pi * self.radius) * np.arccos(self.a / r[~contact])) def _computePressure(self): r = np.sqrt(self.x**2 + self.y**2) self.pressure = np.zeros_like(r) contact = np.where(r < self.a) self.pressure[contact] = \ 2 * self.e_star / (np.pi * self.radius) \ * np.sqrt(self.a**2 - r[contact]**2) def _computeSurface(self): self.surface = -1. / (2 * self.radius) * (self.x**2 + self.y**2) @pytest.fixture(scope="module") def hertz(tamaas_fixture): return HertzFixture(1024, 0.00001) @pytest.fixture(scope="module") def hertz_coarse(tamaas_fixture): return HertzFixture(512, 0.0001) @pytest.fixture(scope="module", params=[tm.PolonskyKeerRey.pressure]) def pkr(hertz, request): model = tm.ModelFactory.createModel(tm.model_type.basic_2d, [hertz.domain_size, hertz.domain_size], [hertz.n, hertz.n]) model.setElasticity(hertz.e_star, 0) solver = tm.PolonskyKeerRey(model, hertz.surface, 1e-12, request.param, request.param) solver.solve(hertz.load) return model, hertz class WestergaardFixture: def __init__(self, n, load): self.domain_size = 1. self.lamda = 1. self.delta = 0.1 self.e_star = 1. self.n = n self.p_star = np.pi * self.e_star * self.delta / self.lamda self.load = load * self.p_star self.a = self.lamda / np.pi \ * np.arcsin(np.sqrt(self.load / self.p_star)) self.x = np.linspace(-self.domain_size / 2., self.domain_size / 2., self.n, endpoint=False, dtype=tm.dtype) self._computeSurface() self._computePressure() self._computeDisplacement() def _computeSurface(self): self.surface = self.delta * np.cos(2 * np.pi * self.x / self.lamda) def _computePressure(self): self.pressure = np.zeros_like(self.surface) contact = np.where(np.abs(self.x) < self.a) self.pressure[contact] = 2 * self.load \ * (np.cos(np.pi * self.x[contact] / self.lamda) / np.sin(np.pi * self.a / self.lamda)**2) \ * np.sqrt(np.sin(np.pi * self.a / self.lamda)**2 - np.sin(np.pi * self.x[contact] / self.lamda)**2) def _computeDisplacement(self): psi = np.pi * np.abs(self.x) / self.lamda psi_a = np.pi * self.a / self.lamda with np.errstate(invalid='ignore'): # get some warnings out of the way self.displacement = (np.cos(2*psi) + 2 * np.sin(psi) * np.sqrt(np.sin(psi)**2 - np.sin(psi_a)**2) - 2 * np.sin(psi_a)**2 * np.log((np.sin(psi) + np.sqrt(np.sin(psi)**2 - np.sin(psi_a)**2)) / np.sin(psi_a))) contact = np.where(np.abs(self.x) < self.a) self.displacement[contact] = np.cos(2*psi[contact]) self.displacement *= self.load * self.lamda / (np.pi * self.e_star * np.sin(psi_a)**2) @pytest.fixture(scope="module") def westergaard(tamaas_fixture): return WestergaardFixture(19683, 0.1) class PatchWestergaard: def __init__(self, model_type, domain, modes, size): self.E = 3. self.nu = 0. self.e_star = self.E / (1 - self.nu**2) self.n = size self.domain = domain self.model = tm.ModelFactory.createModel(model_type, domain, size) self.model.setElasticity(self.E, self.nu) self.pressure = profile(np.cos, size, modes, 1) self.solution = profile(np.cos, size, modes, 1 / (np.pi * self.e_star * norm(modes))) @pytest.fixture(scope="module", params=[tm.model_type.basic_2d]) def patch_westergaard(tamaas_fixture, request): return PatchWestergaard(request.param, [1., 1.], [3, 1], [6, 6]) class UniformPlasticity: def __init__(self, model_type, domain, sizes): self.n = sizes self.domain = domain self.model = tm.ModelFactory.createModel(model_type, domain, sizes) self.E_h = 0.1 self.sigma_y = 0.01 self.residual = tm.ModelFactory.createResidual(self.model, sigma_y=self.sigma_y, hardening=self.E_h) self.model.E = 1. self.model.nu = 0. def solution(self, p): E, nu = self.model.E, self.model.nu E_h, sigma_y = self.E_h, self.sigma_y mu = E / (2 * (1 + nu)) strain = -1 / (mu + E_h) * (p * (3 * mu + E_h) / (2 * mu) - np.sign(p) * sigma_y) dep = (2 * mu * np.abs(strain) - sigma_y) / (3 * mu + E_h) plastic_strain = np.sign(strain) / 2 * dep * np.array([ -1, -1, 2, 0, 0, 0, ], dtype=tm.dtype) stress = 2 * mu * (np.array([ 0, 0, strain, 0, 0, 0 ], dtype=tm.dtype) - plastic_strain) return { "stress": stress, "plastic_strain": plastic_strain, "cumulated_plastic_strain": dep + }, { + "stress": mu, + "plastic_strain": 1, + "cumulated_plastic_strain": 1 } @pytest.fixture(scope="module", params=[tm.model_type.volume_2d]) def patch_isotropic_plasticity(tamaas_fixture, request): return UniformPlasticity(request.param, [1., 1., 1.], [4, 4, 4]) diff --git a/tests/test_patch_plasticity.py b/tests/test_patch_plasticity.py index 424b951..fe0e408 100644 --- a/tests/test_patch_plasticity.py +++ b/tests/test_patch_plasticity.py @@ -1,42 +1,43 @@ # -*- coding: utf-8 -*- # @file # @section LICENSE # # Copyright (©) 2016-20 EPFL (École Polytechnique Fédérale de Lausanne), # Laboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from numpy.linalg import norm from tamaas.nonlinear_solvers import DFSANESolver def test_patch_plasticity(patch_isotropic_plasticity): model = patch_isotropic_plasticity.model residual = patch_isotropic_plasticity.residual applied_pressure = 0.1 solver = DFSANESolver(residual, model) + solver.tolerance = 1e-15 pressure = model['traction'][..., 2] pressure[:] = applied_pressure solver.solve() solver.updateState() - solution = patch_isotropic_plasticity.solution(applied_pressure) + solution, normal = patch_isotropic_plasticity.solution(applied_pressure) for key in solution: - error = norm(model[key] - solution[key]) / applied_pressure - assert error < 3e-15 + error = norm(model[key] - solution[key]) / normal[key] + assert error < 1e-15