diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c3c3d36..e94f6e1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,228 +1,234 @@ stages: - docker - lint - build - test - deploy variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG GIT_SUBMODULE_STRATEGY: recursive BUILD_DIR: build-release cache: key: "$CI_COMMIT_REF_SLUG" # ------------------------------------------------------------------------------ .docker_build: stage: docker image: docker:19.03.12 services: - docker:19.03.12-dind variables: # Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled DOCKER_HOST: tcp://docker:2376 DOCKER_TLS_CERTDIR: "/certs" DEFAULT_IMAGE: $CI_REGISTRY_IMAGE:$CI_DEFAULT_BRANCH before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker pull $DEFAULT_IMAGE-$IMAGE_NAME || true - docker build --cache-from $DEFAULT_IMAGE-$IMAGE_NAME -t $IMAGE_TAG-$IMAGE_NAME -f $DOCKERFILE . - docker push $IMAGE_TAG-$IMAGE_NAME docker build:debian: variables: IMAGE_NAME: debian-stable DOCKERFILE: tests/ci/docker/debian.mpi extends: .docker_build docker build:manylinux: variables: IMAGE_NAME: manylinux DOCKERFILE: tests/ci/docker/manylinux extends: .docker_build docker build:cuda: variables: IMAGE_NAME: cuda DOCKERFILE: tests/ci/docker/ubuntu_lts.cuda extends: .docker_build # ------------------------------------------------------------------------------ .debian_stable: variables: output: ${CI_COMMIT_REF_SLUG}-debian-stable image: ${IMAGE_TAG}-debian-stable .manylinux: variables: output: ${CI_COMMIT_REF_SLUG}-manylinux image: ${IMAGE_TAG}-manylinux .cuda: variables: output: ${CI_COMMIT_REF_SLUG}-cuda image: ${IMAGE_TAG}-cuda # ------------------------------------------------------------------------------ lint: stage: lint extends: - .debian_stable allow_failure: true + variables: + CLANG_PATCH: clang_format.patch + FLAKE_ERRORS: flake8_lint.txt script: - git remote remove upstream || true - git remote add upstream "$CI_PROJECT_URL" - git fetch upstream - '[ -z "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" ] && HEAD="$CI_COMMIT_BEFORE_SHA" || HEAD="upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"' - echo "$HEAD" - - git-clang-format --diff "$HEAD" | tee clang_format.patch - - grep "no modified files to format" clang_format.patch || grep "clang-format did not modify any files" clang_format.patch + - git-clang-format --diff "$HEAD" | tee $CLANG_PATCH + - grep "no modified files to format" $CLANG_PATCH || grep "clang-format did not modify any files" $CLANG_PATCH + - flake8 python/ | tee $FLAKE_ERRORS + - test -s $FLAKE_ERRORS artifacts: when: on_failure paths: - - clang_format.patch + - $CLANG_PATCH + - $FLAKE_ERRORS .build: stage: build variables: COMPILE_LOG: compilation.log script: - scons 2>&1 | tee $COMPILE_LOG artifacts: when: always paths: - build-setup.conf - $COMPILE_LOG - $BUILD_DIR - config.log .ccache: variables: CCACHE_BASEDIR: $CI_PROJECT_DIR/$BUILD_DIR CCACHE_DIR: $CI_PROJECT_DIR/.ccache CCACHE_NOHASDIR: 1 CCACHE_COMPILERCHECK: content CXX: /usr/lib/ccache/g++ OMPI_CXX: ${CXX} cache: key: ${output} policy: pull-push paths: - .ccache after_script: - ccache --show-stats || true build:mpi: extends: - .build - .debian_stable - .ccache before_script: - ccache --zero-stats || true - scons build_tests=True use_googletest=True build_python=True py_exec=python3 use_mpi=True backend=omp fftw_threads=omp verbose=True -h build:cuda: extends: - .build - .cuda before_script: - scons build_tests=True use_googletest=True build_python=True py_exec=python3 use_mpi=False backend=cuda fftw_threads=none verbose=True -h # ------------------------------------------------------------------------------ test:mpi: stage: test dependencies: - build:mpi extends: .debian_stable variables: PYTHONPATH: $CI_PROJECT_DIR/$BUILD_DIR/python TESTS: $BUILD_DIR/tests JUNITXML: results.xml TESTS_LOG: tests.log script: - ls $PYTHONPATH - python3 -c 'import sys; print(sys.path)' - python3 -m pytest -vvv --last-failed --durations=0 --junitxml=$JUNITXML $TESTS 2>&1 | tee $TESTS_LOG after_script: - python3 -m pytest --cache-show || true artifacts: when: always paths: - $JUNITXML - $TESTS_LOG reports: junit: - $JUNITXML cache: key: ${output}-pytest policy: pull-push paths: - .pytest_cache # ------------------------------------------------------------------------------ .protected_refs: extends: .manylinux rules: - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH || $CI_COMMIT_TAG =~ /^v.*/' wheels: stage: build extends: - .protected_refs - .ccache before_script: - ccache --zero-stats || true script: - ./tests/ci/build_wheels.sh artifacts: paths: - dist/wheelhouse # ------------------------------------------------------------------------------ .deploy_wheels: stage: deploy extends: .protected_refs dependencies: - wheels script: python -m twine upload --verbose dist/wheelhouse/* package:gitlab: extends: .deploy_wheels variables: TWINE_USERNAME: gitlab-ci-token TWINE_PASSWORD: ${CI_JOB_TOKEN} TWINE_REPOSITORY_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi package:pypi: extends: .deploy_wheels variables: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${PYPI_TOKEN} rules: - if: '$CI_COMMIT_TAG =~ /^v.*/' diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d8324..cac80ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,233 +1,234 @@ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for final versions and [PEP440](https://www.python.org/dev/peps/pep-0440/) in case intermediate versions need to be released (e.g. development version `2.2.3.dev1` or release candidates `2.2.3rc1`), or individual commits are packaged. ## Unreleased ### Changed - The root `Dockerfile` now compiles Tamaas, so using Tamaas in Docker is easier. - The model attributes dumped to Numpy files are now written in a JSON-formatted string to avoid unsafe loading/unpickling of objects. - Removed the `build_doc` build option: now the doc targets are automatically added if the dependencies are met, and built if `scons doc` is called. - Removed the `use_googletest` build option: if tests are built and gtest is present, the corresponding tests will be built ## v2.3.0 -- 2021-06-15 ### Added - Added `read()` method to dumpers to create a model from a dump file - `getClusters()` can be called in MPI contact with partial contact maps - Added a JSON encoder class for models and a JSON dumper - CUDA compatibility is re-established, but has not been tested - Docstrings in the Python bindings for many classes/methods +- Now using `clang-format` and `flake8` for linting ### Changed - Tamaas version numbers are now managed by [versioneer](https://github.com/python-versioneer/python-versioneer). This means that Git tags prefixed with `v` (e.g. `v2.2.3`) carry meaning and determine the version. When no tag is set, versioneer uses the last tag, specifies the commit short hash and the distance to the last tag (e.g. `2.2.2+33.ge314b0e`). This version string is used in the compiled library, the `setup.py` script and the `__version__` variable in the python module. - Tamaas migrated to [GitLab](https://gitlab.com/tamaas/tamaas) - Continuous delivery has been implemented: - the `master` branch will now automatically build and publish Python wheels to `https://gitlab.com/api/v4/projects/19913787/packages/pypi/simple`. These "nightly" builds can be installed with: pip install \ --extra-index-url https://gitlab.com/api/v4/projects/19913787/packages/pypi/simple \ tamaas - version tags pushed to `master` will automatically publish the wheels to [PyPI](https://pypi.org/project/tamaas/) ### Deprecated - The `finalize()` function is now deprecated, since it is automatically called when the process terminates - Python versions 3.5 and below are not supported anymore ### Fixed - Fixed a host of dump read/write issues when model type was not `volume_*d`. Dumper tests are now streamlined and systematic. - Fixed a bug where `Model::solveDirichlet` would not compute correctly - Fixed a bug where `Statistics::contact` would not normalize by the global number of surface points ## v2.2.2 -- 2021-04-02 ### Added - Entry-point `tamaas` defines a grouped CLI for `examples/pipe_tools`. Try executing `tamaas surface -h` from the command-line! ### Changed - `CXXFLAGS` are now passed to the linker - Added this changelog - Using absolute paths for environmental variables when running `scons test` - Reorganized documentation layout - Gave the build system a facelift (docs are now generated directly with SCons instead of a Makefile) ### Deprecated - Python 2 support is discontinued. Version `v2.2.1` is the last PyPi build with a Python 2 wheel. - The scripts in `examples/pipe_tools` have been replaced by the `tamaas` command ### Fixed - `UVWDumper` no longer imports `mpi4py` in sequential - Compiling with different Thrust/FFTW backends ## v2.2.1 -- 2021-03-02 ### Added - Output registered fields and dumpers in `print(model)` - Added `operator[]` to the C++ model class (for fields) - Added `traction` and `displacement` properties to Python model bindings - Added `operators` property to Python model bindings, which provides a dict-like access to registered operators - Added `shape` and `spectrum` to properties to Python surface generator bindings - Surface generator constructor accepts surface global shape as argument - Choice of FFTW thread model ### Changed - Tests use `/tmp` for temporary files - Updated dependency versions (Thrust, Pybind11) ### Deprecated - Most `get___()` and `set___()` in Python bindings have been deprecated. They will generate a `DeprecationWarning`. ### Removed - All legacy code ## v2.2.0 -- 2020-12-31 ### Added - More accurate function for computation of contact area - Function to compute deviatoric of tensor fields - MPI implementation - Convenience `hdf5toVTK` function - Readonly properties `shape`, `global_shape`, `boundary_shape` on model to give shape information ### Changed - Preprocessor defined macros are prefixed with `TAMAAS_` - Moved `tamaas.to_voigt` to `tamaas.compute.to_voigt` ### Fixed - Warning about deprecated constructors with recent GCC versions - Wrong computation of grid strides - Wrong computation of grid sizes in views ## v2.1.4 -- 2020-08-07 ### Added - Possibility to generate a static `libTamaas` - C++ implementation of DFSANE solver - Allowing compilation without OpenMP ### Changed - NetCDF dumper writes frames to a single file ### Fixed - Compatibility with SCons+Python 3 ## v2.1.3 -- 2020-07-27 ### Added - Version number to `TamaasInfo` ### Changed - Prepending root directory when generating archive ## v2.1.2 -- 2020-07-24 This release changes some core internals related to discrete Fourier transforms for future MPI support. ### Added - Caching `CXXFLAGS` in SCons build - SCons shortcut to create code archive - Test of the elastic-plastic contact solver - Paraview data dumper (`.pvd` files) - Compression for UVW dumper - `__contains__` and `__iter__` Python bindings of model - Warning message of possible overflow in Kelvin ### Changed - Simplified `tamaas_info.cpp`, particularly the diff part - Using a new class `FFTEngine` to manage discrete Fourier transforms. Plans are re-used as much as possible with different data with the same shape. This is in view of future MPI developments - Redirecting I/O streams in solve functions so they can be used from Python (e.g. in Jupyter notebooks) - Calling `initialize()` and `finalize()` is no longer necessary ### Fixed - Convergence issue with non-linear solvers - Memory error in volume potentials ## v2.1.1 -- 2020-04-22 ### Added - SCons shortcut to run tests ### Fixed - Correct `RPATH` for shared libraries - Issues with SCons commands introduced in v2.1.0 - Tests with Python 2.7 ## v2.1.0 -- 2020-04-17 ### Added - SCons shortcuts to build/install Tamaas and its components - Selection of integration method for Kelvin operator - Compilation option to remove the legacy part of Tamaas - NetCDF dumper ### Fixed - Link bug with clang - NaNs in Kato saturated solver ## v2.0.0 -- 2019-11-11 First public release. Contains relatively mature elastic-plastic contact code. diff --git a/python/tamaas/__main__.py b/python/tamaas/__main__.py index bae4bbf..5dcae81 100755 --- a/python/tamaas/__main__.py +++ b/python/tamaas/__main__.py @@ -1,208 +1,207 @@ #!/usr/bin/env python3 # -*- mode: python; coding: utf-8 -*- # vim: set ft=python: # # Copyright (©) 2016-2021 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 . import sys import io import time import argparse import tamaas as tm import numpy as np __author__ = "Lucas Frérot" __copyright__ = ( "Copyright (©) 2019-2021, EPFL (École Polytechnique Fédérale de Lausanne)," "\nLaboratory (LSMS - Laboratoire de Simulation en Mécanique des Solides)" ) __license__ = "SPDX-License-Identifier: AGPL-3.0-or-later" def load_stream(stream): """ Load numpy from binary stream (allows piping) Code from https://gist.github.com/CMCDragonkai/3c99fd4aabc8278b9e17f50494fcc30a """ np_magic = stream.read(6) # use the sys.stdin.buffer to read binary data np_data = stream.read() # read it all into an io.BytesIO object return io.BytesIO(np_magic + np_data) def surface(args): if args.generator == 'random_phase': generator = tm.SurfaceGeneratorRandomPhase2D(args.sizes) elif args.generator == 'filter': generator = tm.SurfaceGeneratorFilter2D(args.sizes) else: raise ValueError('Unknown generator method {}'.format(args.generator)) generator.spectrum = tm.Isopowerlaw2D() generator.spectrum.q0 = args.cutoffs[0] generator.spectrum.q1 = args.cutoffs[1] generator.spectrum.q2 = args.cutoffs[2] generator.spectrum.hurst = args.hurst generator.random_seed = args.seed surface = generator.buildSurface() / generator.spectrum.rmsSlopes() \ * args.rms output = args.output if args.output is not None else sys.stdout params = { 'q0': generator.spectrum.q0, 'q1': generator.spectrum.q1, 'q2': generator.spectrum.q2, 'hurst': generator.spectrum.hurst, 'random_seed': generator.random_seed, 'rms_heights': args.rms, 'generator': args.generator, } try: np.savetxt(output, surface, header=str(params)) except BrokenPipeError: pass def contact(args): from tamaas.dumpers import NumpyDumper tm.set_log_level(tm.LogLevel.error) if not args.input: input = sys.stdin else: input = args.input surface = np.loadtxt(input) discretization = surface.shape system_size = [1., 1.] model = tm.ModelFactory.createModel(tm.model_type.basic_2d, system_size, discretization) solver = tm.PolonskyKeerRey(model, surface, args.tol) solver.solve(args.load) dumper = NumpyDumper('numpy', 'traction', 'displacement') dumper.dump_to_file(sys.stdout.buffer, model) def plot(args): import matplotlib.pyplot as plt fig, (ax_traction, ax_displacement) = plt.subplots(1, 2) ax_traction.set_title('Traction') ax_displacement.set_title('Displacement') with load_stream(sys.stdin.buffer) as f_np: data = np.load(f_np) ax_traction.imshow(data['traction']) ax_displacement.imshow(data['displacement']) fig.set_size_inches(10, 6) fig.tight_layout() plt.show() def main(): parser = argparse.ArgumentParser( prog='tamaas', description=("The tamaas command is a simple utility for surface" " generation, contact computation and" " plotting of contact solutions"), ) subs = parser.add_subparsers(title='commands', description='utility commands') # Arguments for surface command parser_surface = subs.add_parser( 'surface', description='Generate a self-affine rough surface') parser_surface.add_argument("--cutoffs", "-K", nargs=3, type=int, help="Long, rolloff, short wavelength cutoffs", metavar=('k_l', 'k_r', 'k_s'), required=True) parser_surface.add_argument("--sizes", nargs=2, type=int, help="Number of points", metavar=('nx', 'ny'), required=True) parser_surface.add_argument("--hurst", "-H", type=float, help="Hurst exponent", required=True) parser_surface.add_argument("--rms", type=float, help="Root-mean-square of slopes", default=1.) parser_surface.add_argument("--seed", type=int, help="Random seed", default=int(time.time())) parser_surface.add_argument("--generator", help="Generation method", choices=('random_phase', 'filter'), default='random_phase') parser_surface.add_argument("--output", "-o", help="Output file name (compressed if .gz)") parser_surface.set_defaults(func=surface) - # Arguments for contact command parser_contact = subs.add_parser( 'contact', description="Compute the elastic contact solution with a given surface") parser_contact.add_argument("--input", "-i", help="Rough surface file (default stdin)") parser_contact.add_argument("--tol", type=float, default=1e-12, help="Solver tolerance") parser_contact.add_argument("load", type=float, help="Applied average pressure") parser_contact.set_defaults(func=contact) - # Arguments for plot command parser_plot = subs.add_parser( 'plot', description='Plot contact solution') parser_plot.set_defaults(func=plot) args = parser.parse_args() try: args.func(args) except AttributeError: parser.print_usage() + if __name__ == '__main__': main() diff --git a/python/tamaas/nonlinear_solvers/__init__.py b/python/tamaas/nonlinear_solvers/__init__.py index 2535fab..afa9b68 100644 --- a/python/tamaas/nonlinear_solvers/__init__.py +++ b/python/tamaas/nonlinear_solvers/__init__.py @@ -1,166 +1,163 @@ # -*- mode:python; coding: utf-8 -*- # # Copyright (©) 2016-2021 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 functools import wraps -import numpy as np -from scipy.sparse.linalg import LinearOperator, lgmres -from scipy.linalg import norm from scipy.optimize import newton_krylov, root from scipy.optimize.nonlin import NoConvergence from .. import EPSolver, Logger, LogLevel, mpi from .._tamaas import _tolerance_manager from .._tamaas import _DFSANESolver as DFSANECXXSolver __all__ = ['NLNoConvergence', 'DFSANESolver', 'DFSANECXXSolver', 'NewtonKrylovSolver', 'ToleranceManager'] class NLNoConvergence(Exception): """Convergence not reached exception""" class ScipySolver(EPSolver): """ Base class for solvers wrapping SciPy routines """ def __init__(self, residual, _, callback=None): super(ScipySolver, self).__init__(residual) if mpi.size() > 1: raise RuntimeError("Scipy solvers cannot be used with MPI; " "DFSANECXXSolver can be used instead") self.callback = callback self._x = self.getStrainIncrement() self._residual = self.getResidual() self.options = {'ftol': 0, 'fatol': 1e-9} 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() EPSolver.beforeSolve(self) # Scipy root callback def compute_residual(vec): self._residual.computeResidual(vec) return self._residual.getVector().copy() # Solve self._x[...] = self.scipy_solve(compute_residual) # Computing displacements self._residual.computeResidualDisplacement(self._x) def reset(self): "Set solution vector to zero" self._x[...] = 0 class NewtonKrylovSolver(ScipySolver): """ Solve using a finite-difference Newton-Krylov method """ def __init__(self, residual, model=None, callback=None): ScipySolver.__init__(self, residual, model, callback=callback) def scipy_solve(self, compute_residual): "Solve R(delta epsilon) = 0 using a newton-krylov method" try: return newton_krylov(compute_residual, self._x, f_tol=self.tolerance, verbose=True, callback=self.callback) except NoConvergence: raise NLNoConvergence("Newton-Krylov did not converge") class DFSANESolver(ScipySolver): """ Solve using a spectral residual jacobianless method """ def __init__(self, residual, model=None, callback=None): ScipySolver.__init__(self, residual, model, callback=callback) def scipy_solve(self, compute_residual): "Solve R(delta epsilon) = 0 using a df-sane method" solution = root(compute_residual, self._x, method='df-sane', options={'ftol': 0, 'fatol': self.tolerance}, callback=self.callback) Logger().get(LogLevel.info) << \ "DF-SANE/Scipy: {} ({} iterations, {})".format( solution.message, solution.nit, self.tolerance) if not solution.success: raise NLNoConvergence("DF-SANE/Scipy did not converge") return solution.x.copy() def ToleranceManager(start, end, rate): "Decorator to manage tolerance of non-linear solver" # start /= rate # just anticipating first multiplication def actual_decorator(cls): orig_init = cls.__init__ orig_solve = cls.solve orig_update_state = cls.updateState @wraps(cls.__init__) def __init__(obj, *args, **kwargs): orig_init(obj, *args, **kwargs) obj.setToleranceManager(_tolerance_manager(start, end, rate)) @wraps(cls.solve) def new_solve(obj, *args, **kwargs): ftol = obj.tolerance ftol *= rate obj.tolerance = max(ftol, end) return orig_solve(obj, *args, **kwargs) @wraps(cls.updateState) def updateState(obj, *args, **kwargs): obj.tolerance = start return orig_update_state(obj, *args, **kwargs) cls.__init__ = __init__ # cls.solve = new_solve # cls.updateState = updateState return cls return actual_decorator