diff --git a/python/wrap/core.cpp b/python/wrap/core.cpp index f473799..e61c995 100644 --- a/python/wrap/core.cpp +++ b/python/wrap/core.cpp @@ -1,117 +1,120 @@ /* * SPDX-License-Indentifier: AGPL-3.0-or-later * * Copyright (©) 2016-2022 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 . * */ /* -------------------------------------------------------------------------- */ #include "logger.hh" #include "statistics.hh" #include "wrap.hh" #include /* -------------------------------------------------------------------------- */ namespace tamaas { /* -------------------------------------------------------------------------- */ namespace wrap { using namespace py::literals; /* -------------------------------------------------------------------------- */ template void wrapStatistics(py::module& mod) { auto name = makeDimensionName("Statistics", dim); py::class_>(mod, name.c_str()) .def_static("computePowerSpectrum", &Statistics::computePowerSpectrum, py::return_value_policy::move, "Compute PSD of surface") .def_static( "computeAutocorrelation", &Statistics::computeAutocorrelation, py::return_value_policy::move, "Compute autocorrelation of surface") .def_static("computeMoments", &Statistics::computeMoments, "Compute spectral moments") .def_static("computeSpectralRMSSlope", &Statistics::computeSpectralRMSSlope, "Compute hrms' in Fourier space") .def_static("computeRMSHeights", &Statistics::computeRMSHeights, "Compute hrms") .def_static("contact", &Statistics::contact, "tractions"_a, "perimeter"_a = 0, "Compute the (corrected) contact area. Permieter is the " - "total contact perimeter in number of segments."); + "total contact perimeter in number of segments.") + .def_static("graphArea", &Statistics::graphArea, "zdisplacement"_a, + "Compute area defined by function graph."); } /* -------------------------------------------------------------------------- */ void wrapCore(py::module& mod) { // Exposing logging facility py::enum_(mod, "LogLevel") .value("debug", LogLevel::debug) .value("info", LogLevel::info) .value("warning", LogLevel::warning) .value("error", LogLevel::error); mod.def("set_log_level", [](LogLevel level) { Logger::setLevel(level); }); mod.def("get_log_level", Logger::getCurrentLevel); py::class_(mod, "Logger") .def(py::init<>()) - .def("get", - [](Logger& logger, LogLevel level) -> Logger& { - logger.get(level); - return logger; - }, - "Get a logger object for a log level") - .def("__lshift__", - [](Logger& logger, std::string msg) -> Logger& { - auto level = logger.getWishLevel(); - if (level >= Logger::getCurrentLevel() and - not(mpi::rank() != 0 and level == LogLevel::info)) { - py::print(msg, - "file"_a = py::module::import("sys").attr("stderr")); - } - return logger; - }, - "Log a message"); + .def( + "get", + [](Logger& logger, LogLevel level) -> Logger& { + logger.get(level); + return logger; + }, + "Get a logger object for a log level") + .def( + "__lshift__", + [](Logger& logger, std::string msg) -> Logger& { + auto level = logger.getWishLevel(); + if (level >= Logger::getCurrentLevel() and + not(mpi::rank() != 0 and level == LogLevel::info)) { + py::print(msg, + "file"_a = py::module::import("sys").attr("stderr")); + } + return logger; + }, + "Log a message"); wrapStatistics<1>(mod); wrapStatistics<2>(mod); - mod.def("to_voigt", - [](const Grid& field) { - TAMAAS_DEPRECATE("tamaas.to_voigt()", "tamaas.compute.to_voigt()"); + mod.def( + "to_voigt", + [](const Grid& field) { + TAMAAS_DEPRECATE("tamaas.to_voigt()", "tamaas.compute.to_voigt()"); - if (field.getNbComponents() == 9) { - Grid voigt(field.sizes(), 6); - Loop::loop( - [] CUDA_LAMBDA(MatrixProxy in, - SymMatrixProxy out) { - out.symmetrize(in); - }, - range>(field), - range>(voigt)); - return voigt; - } + if (field.getNbComponents() == 9) { + Grid voigt(field.sizes(), 6); + Loop::loop([] CUDA_LAMBDA( + MatrixProxy in, + SymMatrixProxy out) { out.symmetrize(in); }, + range>(field), + range>(voigt)); + return voigt; + } - TAMAAS_EXCEPTION("Wrong number of components to symmetrize"); - }, - "Convert a 3D tensor field to voigt notation", - py::return_value_policy::copy); + TAMAAS_EXCEPTION("Wrong number of components to symmetrize"); + }, + "Convert a 3D tensor field to voigt notation", + py::return_value_policy::copy); } } // namespace wrap /* -------------------------------------------------------------------------- */ } // namespace tamaas diff --git a/src/core/statistics.cpp b/src/core/statistics.cpp index 5cc8bfc..e466dc5 100644 --- a/src/core/statistics.cpp +++ b/src/core/statistics.cpp @@ -1,211 +1,249 @@ /* * SPDX-License-Indentifier: AGPL-3.0-or-later * * Copyright (©) 2016-2022 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 . * */ /* -------------------------------------------------------------------------- */ #include "statistics.hh" #include "fft_engine.hh" #include "loop.hh" #include "static_types.hh" /* -------------------------------------------------------------------------- */ namespace tamaas { template Real Statistics::computeRMSHeights(Grid& surface) { return std::sqrt(surface.var()); } template Real Statistics::computeSpectralRMSSlope(Grid& surface) { const auto h_size = GridHermitian::hermitianDimensions(surface.sizes()); auto wavevectors = FFTEngine::template computeFrequencies(h_size); wavevectors *= 2 * M_PI; // need q for slopes const auto psd = computePowerSpectrum(surface); const Real rms_slope_mean = Loop::reduce( [] CUDA_LAMBDA(VectorProxy q, const Complex& psd_val) { // Checking if we're in the zone that does not have hermitian symmetry if (std::abs(q.back()) < 1e-15) return q.l2squared() * psd_val.real(); return 2 * q.l2squared() * psd_val.real(); }, range>(wavevectors), psd); return std::sqrt(rms_slope_mean); } /* -------------------------------------------------------------------------- */ template GridHermitian Statistics::computePowerSpectrum(Grid& surface) { const auto h_size = GridHermitian::hermitianDimensions(surface.sizes()); GridHermitian psd(h_size, surface.getNbComponents()); FFTEngine::makeEngine()->forward(surface, psd); Real factor = 1. / surface.getGlobalNbPoints(); // Squaring the fourier transform of surface and normalizing Loop::loop( [factor] CUDA_LAMBDA(Complex & c) { c *= factor; c *= conj(c); }, psd); return psd; } /* -------------------------------------------------------------------------- */ template Grid Statistics::computeAutocorrelation(Grid& surface) { Grid acf(surface.sizes(), surface.getNbComponents()); auto psd = computePowerSpectrum(surface); FFTEngine::makeEngine()->backward(acf, psd); acf *= acf.getGlobalNbPoints(); return acf; } /* -------------------------------------------------------------------------- */ template Real Statistics::contact(const Grid& tractions, UInt perimeter) { Real points = 0; UInt nc = tractions.getNbComponents(); switch (nc) { case 1: points = Loop::reduce( [] CUDA_LAMBDA(const Real& t) -> Real { return t > 0; }, tractions); break; case 2: points = Loop::reduce( [] CUDA_LAMBDA(VectorProxy t) -> Real { return t.back() > 0; }, range>(tractions)); break; case 3: points = Loop::reduce( [] CUDA_LAMBDA(VectorProxy t) -> Real { return t.back() > 0; }, range>(tractions)); break; default: TAMAAS_EXCEPTION("Invalid number of components in traction"); } auto area = points / tractions.getGlobalNbPoints(); if (dim == 1) perimeter = 0; // Correction from Yastrebov et al. (Trib. Intl., 2017) // 10.1016/j.triboint.2017.04.023 return area - (M_PI - 1 + std::log(2)) / (24. * tractions.getGlobalNbPoints()) * perimeter; } /* -------------------------------------------------------------------------- */ namespace { template class moment_helper { public: moment_helper(const std::array& exp) : exponent(exp) {} CUDA_LAMBDA Complex operator()(VectorProxy q, const Complex& phi) const { Real mul = 1; for (UInt i = 0; i < dim; ++i) mul *= std::pow(q(i), exponent[i]); // Do not duplicate everything from hermitian symmetry if (std::abs(q.back()) < 1e-15) return mul * phi; return 2 * mul * phi; } private: std::array exponent; }; } // namespace +/* -------------------------------------------------------------------------- */ template <> std::vector Statistics<1>::computeMoments(Grid& surface) { constexpr UInt dim = 1; std::vector moments(3); const auto psd = computePowerSpectrum(surface); auto wavevectors = FFTEngine::template computeFrequencies(psd.sizes()); // we don't multiply by 2 pi because moments are computed with k moments[0] = Loop::reduce(moment_helper{{{0}}}, range(wavevectors), psd) .real(); moments[1] = Loop::reduce(moment_helper{{{2}}}, range(wavevectors), psd) .real(); moments[2] = Loop::reduce(moment_helper{{{4}}}, range(wavevectors), psd) .real(); return moments; } template <> std::vector Statistics<2>::computeMoments(Grid& surface) { constexpr UInt dim = 2; std::vector moments(3); const auto psd = computePowerSpectrum(surface); auto wavevectors = FFTEngine::template computeFrequencies(psd.sizes()); // we don't multiply by 2 pi because moments are computed with k moments[0] = Loop::reduce(moment_helper{{{0, 0}}}, range(wavevectors), psd) .real(); auto m02 = Loop::reduce(moment_helper{{{0, 2}}}, range(wavevectors), psd) .real(); auto m20 = Loop::reduce(moment_helper{{{2, 0}}}, range(wavevectors), psd) .real(); moments[1] = (m02 + m20) / 2; auto m22 = Loop::reduce(moment_helper{{{2, 2}}}, range(wavevectors), psd) .real(); auto m40 = Loop::reduce(moment_helper{{{4, 0}}}, range(wavevectors), psd) .real(); auto m04 = Loop::reduce(moment_helper{{{0, 4}}}, range(wavevectors), psd) .real(); moments[2] = (3 * m22 + m40 + m04) / 3; return moments; } +/* -------------------------------------------------------------------------- */ +template +Real Statistics::graphArea(const Grid& zdisplacement) { + const auto h_size = + GridHermitian::hermitianDimensions(zdisplacement.sizes()); + GridHermitian fourier_disp(h_size, 1), + fourier_gradient(h_size, dim); + Grid gradient(zdisplacement.sizes(), dim); + + // Compute gradient in Fourier domain + FFTEngine::makeEngine()->forward(zdisplacement, fourier_disp); + + auto wavevectors = + FFTEngine::template computeFrequencies(h_size); + wavevectors *= 2 * M_PI; + + Loop::loop( + [] CUDA_LAMBDA(VectorProxy q, + VectorProxy grad, const Complex& f) { + grad = q * f; + grad *= Complex{0, 1}; + }, + range>(wavevectors), + range>(fourier_gradient), fourier_disp); + + FFTEngine::makeEngine()->backward(gradient, fourier_gradient); + + // Integrate area of graph formula + Real factor = Loop::reduce( + [] CUDA_LAMBDA(VectorProxy grad) { + return std::sqrt(1 + grad.l2squared()); + }, + range>(gradient)); + return factor / gradient.getGlobalNbPoints(); + // return 2 * M_PI * factor / gradient.getGlobalNbPoints(); +} + template struct Statistics<1>; template struct Statistics<2>; } // namespace tamaas diff --git a/src/core/statistics.hh b/src/core/statistics.hh index 3cf584f..93afcba 100644 --- a/src/core/statistics.hh +++ b/src/core/statistics.hh @@ -1,55 +1,57 @@ /* * SPDX-License-Indentifier: AGPL-3.0-or-later * * Copyright (©) 2016-2022 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 . * */ /* -------------------------------------------------------------------------- */ #ifndef STATISTICS_HH #define STATISTICS_HH /* -------------------------------------------------------------------------- */ #include "fft_engine.hh" #include "grid.hh" /* -------------------------------------------------------------------------- */ namespace tamaas { /* -------------------------------------------------------------------------- */ /** * @brief Suitcase class for all statistics related functions */ template struct Statistics { /// Compute hrms static Real computeRMSHeights(Grid& surface); /// Compute hrms' in fourier space static Real computeSpectralRMSSlope(Grid& surface); /// Compute PSD of surface static GridHermitian computePowerSpectrum(Grid& surface); /// Compute autocorrelation static Grid computeAutocorrelation(Grid& surface); /// Compute spectral moments static std::vector computeMoments(Grid& surface); /// Compute (corrected) contact area fraction static Real contact(const Grid& tractions, UInt perimeter = 0); + /// Compute the area scaling factor of a periodic graph + static Real graphArea(const Grid& displacement); using PVector = VectorProxy; }; /* -------------------------------------------------------------------------- */ } // namespace tamaas #endif // STATISTICS_HH diff --git a/tests/test_surface.py b/tests/test_surface.py index 6aec50a..368ad91 100644 --- a/tests/test_surface.py +++ b/tests/test_surface.py @@ -1,133 +1,146 @@ # -*- coding: utf-8 -*- # # Copyright (©) 2016-2022 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 print_function, division import numpy as np import tamaas as tm import pytest import scipy.integrate as integrate -from scipy.special import jv +from scipy.special import jv, ellipe as E from numpy import pi from scipy.interpolate import interp2d def spectrum(r, isopowerlaw): kl, kr, ks = isopowerlaw.q0, isopowerlaw.q1, isopowerlaw.q2 H = isopowerlaw.hurst C = 1. if r > ks: return 0 elif r > kr: - return C * (r/float(kr))**(-2.*(H+1)) + return C * (r / float(kr))**(-2. * (H + 1)) elif r > kl: return C else: return 0 def integrate_bessel(x, spectrum, n): - return 2 * pi * integrate.quad(lambda y: y*jv(0, y*x) * spectrum(n*y), - 0, np.inf, limit=200)[0] + return 2 * pi * integrate.quad( + lambda y: y * jv(0, y * x) * spectrum(n * y), 0, np.inf, limit=200)[0] def radial_average(field): x = np.linspace(-1, 1, field.shape[0]) y = np.linspace(-1, 1, field.shape[1]) interpolation = interp2d(x, y, field) n = field.shape[0] // 2 radii = np.linspace(0, 1, n) thetas = np.linspace(0, 2 * pi, endpoint=False) sum = np.zeros_like(radii) for Θ in thetas: for i, r in enumerate(radii): sum[i] += interpolation(r * np.cos(Θ), r * np.sin(Θ)) return sum / thetas.size integrate_bessel = np.vectorize(integrate_bessel, otypes=[tm.dtype]) -@pytest.fixture(scope="module", - params=[tm.SurfaceGeneratorFilter2D, - tm.SurfaceGeneratorRandomPhase2D]) +@pytest.fixture( + scope="module", + params=[tm.SurfaceGeneratorFilter2D, tm.SurfaceGeneratorRandomPhase2D]) def stats(request): iso = tm.Isopowerlaw2D() iso.q0 = 16 iso.q1 = 32 iso.q2 = 64 iso.hurst = 0.8 N = 243 sg = request.param() sg.spectrum = iso sg.shape = [N, N] analytical_stats = { "rms_heights": iso.rmsHeights(), "rms_slopes_spectral": iso.rmsSlopes(), "moments": iso.moments(), "alpha": iso.alpha() } stats = {k: [] for k in analytical_stats} acf = np.zeros(sg.shape, dtype=tm.dtype) realizations = 1000 for i in range(realizations): sg.random_seed = i s = sg.buildSurface() - stats['rms_heights'].append( - tm.Statistics2D.computeRMSHeights(s)) + stats['rms_heights'].append(tm.Statistics2D.computeRMSHeights(s)) stats['rms_slopes_spectral'].append( tm.Statistics2D.computeSpectralRMSSlope(s)) stats['moments'].append(tm.Statistics2D.computeMoments(s)) acf += tm.Statistics2D.computeAutocorrelation(s) / realizations L = 1 * sg.spectrum.q0 n = sg.shape[0] // 2 - x = np.linspace(0, L/2, n) + x = np.linspace(0, L / 2, n) true_acf = (L / (2 * pi))**2 \ * integrate_bessel(x, lambda x: spectrum(x, sg.spectrum), L / (2 * pi)) return analytical_stats, stats, acf, true_acf @pytest.mark.parametrize('key', ['rms_heights', 'rms_slopes_spectral']) def test_statistics(stats, key): analytical_stats, stats, _, _ = stats mu = np.mean(stats[key]) error = np.abs(mu - analytical_stats[key]) / analytical_stats[key] assert error < 3e-3 def test_autocorrelation(stats): _, _, acf, true_acf = stats acf = radial_average(np.fft.fftshift(acf)) - acf_error = np.sum((acf-true_acf)**2) / np.sum(true_acf**2) + acf_error = np.sum((acf - true_acf)**2) / np.sum(true_acf**2) assert acf_error < 2e-3 + + +def test_graph_area(): + N = 100 + x = np.linspace(0, 1, N, endpoint=False) + f = np.sin(2 * np.pi * x) + df = 2 * np.pi * np.cos(2 * np.pi * x) + + num_area = np.sqrt(1 + df**2).mean() + area = 2 * np.sqrt(1 + 4 * np.pi**2) / np.pi * E(4 * np.pi**2 / + (1 + 4 * np.pi**2)) + fourier_area = tm.Statistics1D.graphArea(f) + assert np.abs(num_area - fourier_area) / num_area < 1e-10 + assert np.abs(area - fourier_area) / area < 2e-10