From eb7a9a997059503c25be8c1458900dc8ecbc378f Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 11:06:23 +0100 Subject: [PATCH 01/14] cyl trafo params: auto generated orientation --- src/python/espressomd/math.py | 5 ++ .../CylindricalTransformationParameters.hpp | 28 +++++++---- .../cylindrical_transformation_parameters.hpp | 7 +++ testsuite/python/es_math.py | 48 +++++++++++++++++++ 4 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 testsuite/python/es_math.py diff --git a/src/python/espressomd/math.py b/src/python/espressomd/math.py index 5b7ff893eb4..996364c995c 100644 --- a/src/python/espressomd/math.py +++ b/src/python/espressomd/math.py @@ -32,5 +32,10 @@ class CylindricalTransformationParameters(ScriptInterfaceHelper): Orientation vector of the ``z``-axis of the cylindrical coordinate system. orientation: (3,) array_like of :obj:`float`, default = [1, 0, 0] The axis on which ``phi = 0``. + + Notes + ----- + If you provide no arguments, the defaults above are set. + If you provide only a ``center`` and an ``axis``, an ``orientation`` will be automatically generated that is orthogonal to ``axis``. """ _so_name = "CylindricalTransformationParameters" diff --git a/src/script_interface/CylindricalTransformationParameters.hpp b/src/script_interface/CylindricalTransformationParameters.hpp index 89ed4b840d0..6e60121c9d3 100644 --- a/src/script_interface/CylindricalTransformationParameters.hpp +++ b/src/script_interface/CylindricalTransformationParameters.hpp @@ -22,6 +22,8 @@ #ifndef SCRIPT_INTERFACE_CYL_TRANSFORM_PARAMS_HPP #define SCRIPT_INTERFACE_CYL_TRANSFORM_PARAMS_HPP +#include + #include "script_interface/ScriptInterface.hpp" #include "utils/math/cylindrical_transformation_parameters.hpp" @@ -44,14 +46,24 @@ class CylindricalTransformationParameters return m_transform_params; } void do_construct(VariantMap const ¶ms) override { - m_transform_params = - std::make_shared( - get_value_or(params, "center", - Utils::Vector3d{{0, 0, 0}}), - get_value_or(params, "axis", - Utils::Vector3d{{0, 0, 1}}), - get_value_or(params, "orientation", - Utils::Vector3d{{1, 0, 0}})); + auto n_params = params.size(); + switch(n_params){ + case 0: m_transform_params = + std::make_shared(); + break; + case 2: m_transform_params = + std::make_shared( + get_value(params, "center"), + get_value(params, "axis")); + break; + case 3: m_transform_params = + std::make_shared( + get_value(params, "center"), + get_value(params, "axis"), + get_value(params, "orientation")); + break; + default: throw std::runtime_error("Provide either no arguments, center and axis, or center and axis and orientation"); + } } private: diff --git a/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp b/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp index 20f18d78a77..68b7b173c71 100644 --- a/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp +++ b/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp @@ -23,6 +23,7 @@ #include #include +#include namespace Utils { @@ -44,6 +45,12 @@ class CylindricalTransformationParameters { : m_center(center), m_axis(axis), m_orientation(orientation) { validate(); } + /** + * @brief if you only provide center and axis, an orientation will be generated automatically such that it is orthogonal to axis + */ + CylindricalTransformationParameters(Utils::Vector3d const ¢er, + Utils::Vector3d const &axis) + : m_center(center), m_axis(axis), m_orientation(Utils::calc_orthonormal_vector(axis)) {} Utils::Vector3d center() const { return m_center; } Utils::Vector3d axis() const { return m_axis; } diff --git a/testsuite/python/es_math.py b/testsuite/python/es_math.py new file mode 100644 index 00000000000..32660025679 --- /dev/null +++ b/testsuite/python/es_math.py @@ -0,0 +1,48 @@ +# Copyright (C) 2010-2019 The ESPResSo project +# +# This file is part of ESPResSo. +# +# ESPResSo 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. +# +# ESPResSo 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 this program. If not, see . +import numpy as np +import unittest as ut +import espressomd.math + +class TestMath(ut.TestCase): + + def check_orthonormality(self,vec1, vec2): + self.assertAlmostEqual(np.linalg.norm(vec1),1.) + self.assertAlmostEqual(np.linalg.norm(vec2),1.) + self.assertAlmostEqual(np.dot(vec1, vec2), 0) + + def test_cylindrical_transformation_parameters(self): + + """ Test for the varous constructors of CylindricalTransformationParameters """ + + ctp_default = espressomd.math.CylindricalTransformationParameters() + self.check_orthonormality(ctp_default.axis, ctp_default.orientation) + + axis = np.array([-17, 0.1, np.pi]) + axis /= np.linalg.norm(axis) + ctp_auto_orientation = espressomd.math.CylindricalTransformationParameters(center = 3*[42], axis = axis) + self.check_orthonormality(ctp_auto_orientation.axis, ctp_auto_orientation.orientation) + + ctp_full = espressomd.math.CylindricalTransformationParameters(center = 3*[42], axis = [0,1,0], orientation = [1,0,0]) + self.check_orthonormality(ctp_full.axis, ctp_full.orientation) + + with self.assertRaises(Exception): + ctp_only_center = espressomd.math.CylindricalTransformationParameters(center = 3*[42]) + +if __name__ == "__main__": + ut.main() + From fa5ccc8b7b5f46a51c69e6fa6deb4c6d6c158946 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 11:57:45 +0100 Subject: [PATCH 02/14] hcf: use cyl trafo params --- .../shapes/HollowConicalFrustum.hpp | 34 +++++++++++------- .../include/shapes/HollowConicalFrustum.hpp | 36 ++++++++----------- src/shapes/src/HollowConicalFrustum.cpp | 11 +++--- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/script_interface/shapes/HollowConicalFrustum.hpp b/src/script_interface/shapes/HollowConicalFrustum.hpp index ba3fe013d1c..206db83a662 100644 --- a/src/script_interface/shapes/HollowConicalFrustum.hpp +++ b/src/script_interface/shapes/HollowConicalFrustum.hpp @@ -21,25 +21,16 @@ #define ESPRESSO_HOLLOW_CONICAL_FRUSTUM_HPP #include "Shape.hpp" #include +#include namespace ScriptInterface { namespace Shapes { class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum() - : m_hollow_conical_frustum(new ::Shapes::HollowConicalFrustum()) { - add_parameters( - {{"center", - [this](Variant const &v) { - m_hollow_conical_frustum->set_center(get_value(v)); - }, - [this]() { return m_hollow_conical_frustum->center(); }}, - {"axis", - [this](Variant const &v) { - m_hollow_conical_frustum->set_axis(get_value(v)); - }, - [this]() { return m_hollow_conical_frustum->axis(); }}, + HollowConicalFrustum(){ + add_parameters({ + {"cyl_transform_params", m_cyl_transform_params}, {"r1", [this](Variant const &v) { m_hollow_conical_frustum->set_r1(get_value(v)); @@ -67,12 +58,29 @@ class HollowConicalFrustum : public Shape { [this]() { return m_hollow_conical_frustum->direction(); }}}); } + void do_construct(VariantMap const ¶ms) override { + set_from_args(m_cyl_transform_params, params, "cyl_transform_params"); + + if (m_cyl_transform_params) + m_hollow_conical_frustum = std::make_shared<::Shapes::HollowConicalFrustum>( + get_value(params,"r1"), + get_value(params,"r2"), + get_value(params,"length"), + get_value(params,"thickness"), + get_value(params,"direction"), + m_cyl_transform_params->cyl_transform_params() + + ); + + } + std::shared_ptr<::Shapes::Shape> shape() const override { return m_hollow_conical_frustum; } private: std::shared_ptr<::Shapes::HollowConicalFrustum> m_hollow_conical_frustum; + std::shared_ptr m_cyl_transform_params; }; } /* namespace Shapes */ diff --git a/src/shapes/include/shapes/HollowConicalFrustum.hpp b/src/shapes/include/shapes/HollowConicalFrustum.hpp index 1deb0d98f2e..a8cae52aba4 100644 --- a/src/shapes/include/shapes/HollowConicalFrustum.hpp +++ b/src/shapes/include/shapes/HollowConicalFrustum.hpp @@ -22,9 +22,11 @@ #include "Shape.hpp" #include -#include +#include "utils/math/cylindrical_transformation_parameters.hpp" #include +#include +#include namespace Shapes { @@ -48,24 +50,18 @@ namespace Shapes { */ class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum() - : m_r1(0.0), m_r2(0.0), m_length(0.0), m_thickness(0.0), - m_direction(1), m_center{Utils::Vector3d{}}, m_axis{Utils::Vector3d{ - 0., 0., 1.}}, - m_orientation{Utils::Vector3d{1., 0., 0.}} {} + HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, std::shared_ptr ctp) + : m_r1(r1), m_r2(r2), m_length(length), m_thickness(thickness), + m_direction(direction), m_cyl_transform_params(std::move(ctp)) {} void set_r1(double const radius) { m_r1 = radius; } void set_r2(double const radius) { m_r2 = radius; } void set_length(double const length) { m_length = length; } void set_thickness(double const thickness) { m_thickness = thickness; } void set_direction(int const dir) { m_direction = dir; } - void set_axis(Utils::Vector3d const &axis) { - m_axis = axis; - // Even though the HCF is cylinder-symmetric, it needs a well defined phi=0 - // orientation for the coordinate transformation. - m_orientation = Utils::calc_orthonormal_vector(axis); + void set_cyl_transform_params(std::shared_ptr ctp) { + m_cyl_transform_params = std::move(ctp); } - void set_center(Utils::Vector3d const ¢er) { m_center = center; } /// Get radius 1 perpendicular to axis. double radius1() const { return m_r1; } @@ -75,13 +71,11 @@ class HollowConicalFrustum : public Shape { double length() const { return m_length; } /// Get thickness of the frustum. double thickness() const { return m_thickness; } - /// Get the direction of the shape. If -1, distance is positive within the - /// enclosed volume of the frustum. - int direction() const { return m_direction; } - /// Get center of symmetry. - Utils::Vector3d const ¢er() const { return m_center; } - /// Get symmetry axis. - Utils::Vector3d const &axis() const { return m_axis; } + /// Get direction + int direction() const {return m_direction;} + /// Get cylindrical transformation parameters + std::shared_ptr cyl_transform_params() const {return m_cyl_transform_params;} + /** * @brief Calculate the distance vector and its norm between a given position * and the cone. @@ -98,9 +92,7 @@ class HollowConicalFrustum : public Shape { double m_length; double m_thickness; int m_direction; - Utils::Vector3d m_center; - Utils::Vector3d m_axis; - Utils::Vector3d m_orientation; + std::shared_ptr m_cyl_transform_params; }; } // namespace Shapes diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index ea384252cc4..34d7ee48c31 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -31,11 +31,14 @@ namespace Shapes { void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, double &dist, Utils::Vector3d &vec) const { + auto const center = m_cyl_transform_params->center(); + auto const axis = m_cyl_transform_params->axis(); + auto const orientation = m_cyl_transform_params->orientation(); // transform given position to cylindrical coordinates in the reference frame // of the cone - auto const v = pos - m_center; + auto const v = pos - center; auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( - v, m_axis, m_orientation); + v, axis, orientation); // clang-format off /* * the following implementation is based on: @@ -62,8 +65,8 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, // Transform back to cartesian coordinates. auto const pos_intersection = Utils::transform_coordinate_cylinder_to_cartesian( - {r_intersection, pos_cyl[1], z_intersection}, m_axis, m_orientation) + - m_center; + {r_intersection, pos_cyl[1], z_intersection}, axis, orientation) + + center; auto const u = (pos - pos_intersection).normalize(); auto const d = (pos - pos_intersection).norm() - 0.5 * m_thickness; From 2cf004a243cb0212e76f8a23f6aeab63e63ceb95 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 12:27:30 +0100 Subject: [PATCH 03/14] adapt tests to new signature --- .../shapes/HollowConicalFrustum.hpp | 2 +- .../include/shapes/HollowConicalFrustum.hpp | 4 ++-- .../unit_tests/HollowConicalFrustum_test.cpp | 15 ++++-------- testsuite/python/constraint_shape_based.py | 24 +++++++++---------- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/script_interface/shapes/HollowConicalFrustum.hpp b/src/script_interface/shapes/HollowConicalFrustum.hpp index 206db83a662..5f9c1c67724 100644 --- a/src/script_interface/shapes/HollowConicalFrustum.hpp +++ b/src/script_interface/shapes/HollowConicalFrustum.hpp @@ -67,7 +67,7 @@ class HollowConicalFrustum : public Shape { get_value(params,"r2"), get_value(params,"length"), get_value(params,"thickness"), - get_value(params,"direction"), + get_value_or(params,"direction",1), m_cyl_transform_params->cyl_transform_params() ); diff --git a/src/shapes/include/shapes/HollowConicalFrustum.hpp b/src/shapes/include/shapes/HollowConicalFrustum.hpp index a8cae52aba4..711bdd766a5 100644 --- a/src/shapes/include/shapes/HollowConicalFrustum.hpp +++ b/src/shapes/include/shapes/HollowConicalFrustum.hpp @@ -50,9 +50,9 @@ namespace Shapes { */ class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, std::shared_ptr ctp) + HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, std::shared_ptr cyl_transform_params) : m_r1(r1), m_r2(r2), m_length(length), m_thickness(thickness), - m_direction(direction), m_cyl_transform_params(std::move(ctp)) {} + m_direction(direction), m_cyl_transform_params(std::move(cyl_transform_params)) {} void set_r1(double const radius) { m_r1 = radius; } void set_r2(double const radius) { m_r2 = radius; } diff --git a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp index 9407c9cdc0d..d2d731f0d3f 100644 --- a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp +++ b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp @@ -26,6 +26,7 @@ #include #include +#include #include @@ -37,11 +38,8 @@ BOOST_AUTO_TEST_CASE(dist_function) { constexpr double eps = 10 * std::numeric_limits::epsilon(); { - Shapes::HollowConicalFrustum c; - c.set_r1(R1); - c.set_r2(R2); - c.set_length(L); - c.set_axis(Utils::Vector3d{0, 0, 1}); + auto ctp = std::make_shared(); + Shapes::HollowConicalFrustum c(R1,R2, L, 0.,1,ctp); auto pos = Utils::Vector3d{0.0, 0.0, L / 2.0}; Utils::Vector3d vec; @@ -68,11 +66,8 @@ BOOST_AUTO_TEST_CASE(dist_function) { BOOST_CHECK_SMALL(dist - .5, eps); } { - Shapes::HollowConicalFrustum c; - c.set_r1(R1); - c.set_r2(R2); - c.set_length(L); - c.set_axis(Utils::Vector3d{1, 0, 0}); + auto ctp = std::make_shared(Utils::Vector3d{{0.,0.,0.}}, Utils::Vector3d{{1.,0.,0.}}); + Shapes::HollowConicalFrustum c(R1,R2, L, 0.,1,ctp); auto pos = Utils::Vector3d{L / 2.0, 0.0, 0.0}; Utils::Vector3d vec; diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index f3ea78b9456..7d83e40a7bb 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -21,6 +21,7 @@ import math import espressomd +import espressomd.math import espressomd.interactions import espressomd.shapes import tests_common @@ -56,37 +57,35 @@ def test_hollow_conical_frustum(self): def z(y, r1, r2, l): return l / (r1 - r2) * \ y + l / 2. - l * r1 / (r1 - r2) - - shape = espressomd.shapes.HollowConicalFrustum(center=[0.0, 0.0, 0.0], axis=[ - 0, 0, 1], r1=R1, r2=R2, thickness=0.0, length=LENGTH) + + ctp = espressomd.math.CylindricalTransformationParameters() + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=0.0, length=LENGTH) + y_vals = np.linspace(R1, R2, 100) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], 0.0) - shape = espressomd.shapes.HollowConicalFrustum(center=[0.0, 0.0, 0.0], axis=[ - 0, 0, 1], r1=R1, r2=R2, thickness=D, length=LENGTH, direction=-1) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH, direction=-1) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], 0.5 * D) - np.testing.assert_almost_equal(np.copy(shape.center), [0.0, 0.0, 0.0]) - np.testing.assert_almost_equal(np.copy(shape.axis), [0, 0, 1]) + np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.center), [0.0, 0.0, 0.0]) + np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.axis), [0, 0, 1]) self.assertEqual(shape.r1, R1) self.assertEqual(shape.r2, R2) self.assertEqual(shape.thickness, D) self.assertEqual(shape.length, LENGTH) self.assertEqual(shape.direction, -1) - shape = espressomd.shapes.HollowConicalFrustum(center=[0.0, 0.0, 0.0], axis=[ - 0, 0, 1], r1=R1, r2=R2, thickness=D, length=LENGTH) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], -0.5 * D) # check sign of dist - shape = espressomd.shapes.HollowConicalFrustum(center=[0.0, 0.0, 0.0], axis=[ - 0, 0, 1], r1=R1, r2=R1, thickness=D, length=LENGTH) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness=D, length=LENGTH) self.assertLess(shape.calc_distance( position=[0.0, R1, 0.25 * LENGTH])[0], 0.0) self.assertLess(shape.calc_distance( @@ -96,8 +95,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ self.assertGreater(shape.calc_distance( position=[0.0, R1 - (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) - shape = espressomd.shapes.HollowConicalFrustum(center=[0.0, 0.0, 0.0], axis=[ - 0, 0, 1], r1=R1, r2=R1, thickness=D, length=LENGTH, direction=-1) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness=D, length=LENGTH, direction=-1) self.assertGreater(shape.calc_distance( position=[0.0, R1, 0.25 * LENGTH])[0], 0.0) self.assertGreater(shape.calc_distance( From dbf77f81234f84e21e5f09bbabe9b556da0ea1a7 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 13:32:26 +0100 Subject: [PATCH 04/14] implement central angle gap for HCF --- .../shapes/HollowConicalFrustum.hpp | 10 ++++-- .../include/shapes/HollowConicalFrustum.hpp | 17 +++++----- src/shapes/src/HollowConicalFrustum.cpp | 32 ++++++++++++------- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/script_interface/shapes/HollowConicalFrustum.hpp b/src/script_interface/shapes/HollowConicalFrustum.hpp index 5f9c1c67724..287151cf4a6 100644 --- a/src/script_interface/shapes/HollowConicalFrustum.hpp +++ b/src/script_interface/shapes/HollowConicalFrustum.hpp @@ -55,7 +55,12 @@ class HollowConicalFrustum : public Shape { [this](Variant const &v) { m_hollow_conical_frustum->set_direction(get_value(v)); }, - [this]() { return m_hollow_conical_frustum->direction(); }}}); + [this]() { return m_hollow_conical_frustum->direction(); }}, + {"central_angle", + [this](Variant const &v) { + m_hollow_conical_frustum->set_central_angle(get_value(v)); + }, + [this]() { return m_hollow_conical_frustum->central_angle(); }}}); } void do_construct(VariantMap const ¶ms) override { @@ -66,8 +71,9 @@ class HollowConicalFrustum : public Shape { get_value(params,"r1"), get_value(params,"r2"), get_value(params,"length"), - get_value(params,"thickness"), + get_value_or(params,"thickness",0.), get_value_or(params,"direction",1), + get_value_or(params, "central_angle",0.), m_cyl_transform_params->cyl_transform_params() ); diff --git a/src/shapes/include/shapes/HollowConicalFrustum.hpp b/src/shapes/include/shapes/HollowConicalFrustum.hpp index 711bdd766a5..ede3b7853a3 100644 --- a/src/shapes/include/shapes/HollowConicalFrustum.hpp +++ b/src/shapes/include/shapes/HollowConicalFrustum.hpp @@ -50,31 +50,30 @@ namespace Shapes { */ class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, std::shared_ptr cyl_transform_params) + HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, double const central_angle, std::shared_ptr cyl_transform_params) : m_r1(r1), m_r2(r2), m_length(length), m_thickness(thickness), - m_direction(direction), m_cyl_transform_params(std::move(cyl_transform_params)) {} + m_direction(direction), m_central_angle(central_angle), m_cyl_transform_params(std::move(cyl_transform_params)) {} void set_r1(double const radius) { m_r1 = radius; } void set_r2(double const radius) { m_r2 = radius; } void set_length(double const length) { m_length = length; } void set_thickness(double const thickness) { m_thickness = thickness; } void set_direction(int const dir) { m_direction = dir; } - void set_cyl_transform_params(std::shared_ptr ctp) { - m_cyl_transform_params = std::move(ctp); - } + void set_central_angle(double const central_angle) {m_central_angle = central_angle; } /// Get radius 1 perpendicular to axis. double radius1() const { return m_r1; } /// Get radius 2 perpendicular to axis. double radius2() const { return m_r2; } - /// Get length of the frustum (modulo thickness). + /// Get length of the frustum (without thickness). double length() const { return m_length; } /// Get thickness of the frustum. double thickness() const { return m_thickness; } /// Get direction int direction() const {return m_direction;} - /// Get cylindrical transformation parameters - std::shared_ptr cyl_transform_params() const {return m_cyl_transform_params;} + /// Get central angle + double central_angle() const {return m_central_angle; } + /** * @brief Calculate the distance vector and its norm between a given position @@ -92,7 +91,9 @@ class HollowConicalFrustum : public Shape { double m_length; double m_thickness; int m_direction; + double m_central_angle; std::shared_ptr m_cyl_transform_params; + }; } // namespace Shapes diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index 34d7ee48c31..97bd32d1d24 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -20,6 +20,8 @@ #include #include +#include +#include #include #include @@ -31,14 +33,12 @@ namespace Shapes { void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, double &dist, Utils::Vector3d &vec) const { - auto const center = m_cyl_transform_params->center(); - auto const axis = m_cyl_transform_params->axis(); - auto const orientation = m_cyl_transform_params->orientation(); + // transform given position to cylindrical coordinates in the reference frame // of the cone - auto const v = pos - center; + auto const v = pos - m_cyl_transform_params->center(); auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( - v, axis, orientation); + v, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); // clang-format off /* * the following implementation is based on: @@ -50,6 +50,7 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, * r_cone(z) = m * z + (r1 + r2) / 2, -l/2 <= z <= l/2 with m = (r1 - r2) / l * r_normal(z) = -1/m * z + r_pos + 1 / m * z_pos * r_cone = r_normal => z_intersection = (-m*(r1+r2-2*r_pos)+2*z_pos) / (2*(1+m**2)) + * note: z_intersection is also correct for m = 0. */ // clang-format on auto const m = (m_r1 - m_r2) / m_length; @@ -62,16 +63,23 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, return (m_r1 - m_r2) / m_length * z + (m_r1 + m_r2) / 2.0; }; auto const r_intersection = r_cone(z_intersection); - // Transform back to cartesian coordinates. - auto const pos_intersection = - Utils::transform_coordinate_cylinder_to_cartesian( - {r_intersection, pos_cyl[1], z_intersection}, axis, orientation) + - center; + + // Get the angle of the closest point on the cone mantle. + // If pos is in the gap defined by central_angle, the closest point is on the edge of the cone mantle piece. + // Otherwise, we stay in the plane defined by axis and pos + double const phi_closest = Utils::abs(pos_cyl[1])>m_central_angle/2. ? pos_cyl[1] : Utils::sgn(pos_cyl[1]) * m_central_angle/2.; + + // Transform back to cartesian coordinates. + auto const pos_intersection = + Utils::transform_coordinate_cylinder_to_cartesian( + {r_intersection, phi_closest, z_intersection}, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + + m_cyl_transform_params->center(); + + auto const u = (pos - pos_intersection).normalize(); auto const d = (pos - pos_intersection).norm() - 0.5 * m_thickness; - auto const in_out = (d < 0.0) ? -m_direction : m_direction; - dist = in_out * std::abs(d); + dist = d * m_direction; vec = d * u; } } // namespace Shapes From 3a7256723696905aafcfbd253b33e566b95243c4 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 14:25:26 +0100 Subject: [PATCH 05/14] test HCF with central angle --- testsuite/python/constraint_shape_based.py | 59 +++++++++++++++++----- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index 7d83e40a7bb..1b2fc76c25e 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -54,13 +54,27 @@ def test_hollow_conical_frustum(self): R2 = 10.0 LENGTH = 15.0 D = 2.4 + + # test attributes + ctp = espressomd.math.CylindricalTransformationParameters(center = 3*[5], axis = [1.,0.,0.]) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, direction = -1, length=LENGTH, central_angle = np.pi) + + np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.center), 3*[5]) + self.assertAlmostEqual(shape.r1, R1) + self.assertAlmostEqual(shape.r2, R2) + self.assertAlmostEqual(shape.thickness, D) + self.assertAlmostEqual(shape.length, LENGTH) + self.assertEqual(shape.direction, -1) + self.assertAlmostEqual(shape.central_angle, np.pi) - def z(y, r1, r2, l): return l / (r1 - r2) * \ - y + l / 2. - l * r1 / (r1 - r2) + # test points on and inside of the shape ctp = espressomd.math.CylindricalTransformationParameters() shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=0.0, length=LENGTH) + def z(y, r1, r2, l): return l / (r1 - r2) * \ + y + l / 2. - l * r1 / (r1 - r2) + y_vals = np.linspace(R1, R2, 100) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) @@ -70,20 +84,12 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], 0.5 * D) - - np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.center), [0.0, 0.0, 0.0]) - np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.axis), [0, 0, 1]) - self.assertEqual(shape.r1, R1) - self.assertEqual(shape.r2, R2) - self.assertEqual(shape.thickness, D) - self.assertEqual(shape.length, LENGTH) - self.assertEqual(shape.direction, -1) - + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) - self.assertAlmostEqual(dist[0], -0.5 * D) - + self.assertAlmostEqual(dist[0], -0.5 * D) + # check sign of dist shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness=D, length=LENGTH) self.assertLess(shape.calc_distance( @@ -104,6 +110,33 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ position=[0.0, R1 + (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) self.assertLess(shape.calc_distance( position=[0.0, R1 - (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) + + # test points outside of the shape + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH, direction=1) + + dist = shape.calc_distance(position = [R1, 0, LENGTH/2.+5]) + self.assertAlmostEqual(dist[0], 5-D/2.) + np.testing.assert_array_almost_equal(dist[1], [0,0,dist[0]]) + + dist = shape.calc_distance(position = [0.1, 0, LENGTH/2.]) + self.assertAlmostEqual(dist[0], R1-D/2.-0.1) + np.testing.assert_array_almost_equal(dist[1], [-dist[0],0,0]) + + # check rotated coordinates, central angle with straight frustum + CENTER = np.array(3*[5]) + CENTRAL_ANGLE = np.pi/2. + ctp = espressomd.math.CylindricalTransformationParameters(center = CENTER, axis = [1.,0.,0.], orientation = [0.,0.,1.]) + shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness = 0., length=LENGTH, central_angle = CENTRAL_ANGLE) + probe_pos = CENTER + [0, 0.1, 10] + closest_on_surface = CENTER + [0 , R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.)] + dist = shape.calc_distance(position = probe_pos) + d_vec_expected = probe_pos-closest_on_surface + self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) + + # check central angle with funnel-type frustum + + def test_simplepore(self): """ From 1158b9d92f56459b403003fa3a229f7056503752 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 16:51:56 +0100 Subject: [PATCH 06/14] fix central angle handling, rewrite HCF distance calculation --- src/shapes/src/HollowConicalFrustum.cpp | 89 ++++++++++++---------- testsuite/python/constraint_shape_based.py | 28 +++++-- 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index 97bd32d1d24..dcbc341f00a 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -21,15 +21,9 @@ #include #include -#include #include -#include - -#include - namespace Shapes { - void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, double &dist, Utils::Vector3d &vec) const { @@ -39,46 +33,61 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, auto const v = pos - m_cyl_transform_params->center(); auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( v, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); - // clang-format off - /* - * the following implementation is based on: - * - defining the cone in the cylindrical 2d coordinates with e_z = m_axis - * and r in the plane of axis and pos (1) - * - defining the normal to the cone (2) - * - find the intersection between (1) and (2) - * - * r_cone(z) = m * z + (r1 + r2) / 2, -l/2 <= z <= l/2 with m = (r1 - r2) / l - * r_normal(z) = -1/m * z + r_pos + 1 / m * z_pos - * r_cone = r_normal => z_intersection = (-m*(r1+r2-2*r_pos)+2*z_pos) / (2*(1+m**2)) - * note: z_intersection is also correct for m = 0. - */ - // clang-format on - auto const m = (m_r1 - m_r2) / m_length; - auto z_intersection = (-m * (m_r1 + m_r2 - 2 * pos_cyl[0]) + 2 * pos_cyl[2]) / - (2 * (1 + m * m)); - // Limit the possible z values to the range in which the cone exists. - z_intersection = - boost::algorithm::clamp(z_intersection, -m_length / 2.0, m_length / 2.0); - auto r_cone = [this](auto z) { - return (m_r1 - m_r2) / m_length * z + (m_r1 + m_r2) / 2.0; + + auto project_on_line = [](auto const vec, auto const line_start, auto const line_director) { + return line_start + line_director *((vec-line_start)*line_director); }; - auto const r_intersection = r_cone(z_intersection); - // Get the angle of the closest point on the cone mantle. - // If pos is in the gap defined by central_angle, the closest point is on the edge of the cone mantle piece. - // Otherwise, we stay in the plane defined by axis and pos - double const phi_closest = Utils::abs(pos_cyl[1])>m_central_angle/2. ? pos_cyl[1] : Utils::sgn(pos_cyl[1]) * m_central_angle/2.; + Utils::Vector3d pos_closest; + if (Utils::abs(pos_cyl[1])>=m_central_angle/2.){ + // Go to 2d, find the projection onto the cone mantle + auto const pos_2d = Utils::Vector2d{{pos_cyl[0], pos_cyl[2]}}; + auto const r1_endpoint = Utils::Vector2d{{m_r1,m_length/2.}}; + auto const r2_endpoint = Utils::Vector2d{{m_r2,-m_length/2.}}; + auto const line_director = (r2_endpoint-r1_endpoint).normalized(); + auto closest_point_2d = project_on_line(pos_2d, r1_endpoint, line_director); + + // correct projection if it is outside the frustum + if (Utils::abs(closest_point_2d[1])>m_length/2.){ + bool at_r1 = closest_point_2d[1]>0; + closest_point_2d[0] = at_r1 ? m_r1 : m_r2; + closest_point_2d[1] = at_r1 ? m_length/2. : -m_length/2.; + } + + // Go back to cartesian coordinates of the box frame + pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( + {closest_point_2d[0], pos_cyl[1], closest_point_2d[1]}, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + + m_cyl_transform_params->center(); + + } + else{ + // We cannot go to 2d because the central-angle-gap breaks rotational symmetry, + // so we have to get the line endpoints of the closer edge in 3d cartesian coordinates (but still in the reference frame of the HCF) - // Transform back to cartesian coordinates. - auto const pos_intersection = - Utils::transform_coordinate_cylinder_to_cartesian( - {r_intersection, phi_closest, z_intersection}, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + - m_cyl_transform_params->center(); + // Cannot use Utils::sgn because of pos_cyl[1]==0 corner case + auto const endpoint_angle = pos_cyl[1]>=0 ? m_central_angle/2. : -m_central_angle/2.; + auto const r1_endpoint = Utils::transform_coordinate_cylinder_to_cartesian(Utils::Vector3d{{m_r1,endpoint_angle,m_length/2.}}); + auto const r2_endpoint = Utils::transform_coordinate_cylinder_to_cartesian(Utils::Vector3d{{m_r2,endpoint_angle,-m_length/2.}}); + auto const line_director = (r2_endpoint-r1_endpoint).normalized(); + auto const pos_hcf_frame = Utils::transform_coordinate_cylinder_to_cartesian(pos_cyl); + auto pos_closest_hcf_frame = project_on_line(pos_hcf_frame, r1_endpoint, line_director); + // Go back to cylindrical coordinates (HCF reference frame), here we can apply the capping at z = plusminus l/2. + auto pos_closest_hcf_cyl = Utils::transform_coordinate_cartesian_to_cylinder(pos_closest_hcf_frame); + if (Utils::abs(pos_closest_hcf_cyl[2])>m_length/2.){ + bool at_r1 = pos_closest_hcf_cyl[2]>0.; + pos_closest_hcf_cyl[0] = at_r1 ? m_r1 : m_r2; + pos_closest_hcf_cyl[2] = at_r1 ? m_length/2. : -m_length/2.; + } + // Finally, go to cartesian coordinates of the box frame + pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( + pos_closest_hcf_cyl, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + + m_cyl_transform_params->center(); + } - auto const u = (pos - pos_intersection).normalize(); - auto const d = (pos - pos_intersection).norm() - 0.5 * m_thickness; + auto const u = (pos - pos_closest).normalize(); + auto const d = (pos - pos_closest).norm() - 0.5 * m_thickness; dist = d * m_direction; vec = d * u; } diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index 1b2fc76c25e..b3eb2878371 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -123,19 +123,37 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ np.testing.assert_array_almost_equal(dist[1], [-dist[0],0,0]) # check rotated coordinates, central angle with straight frustum - CENTER = np.array(3*[5]) - CENTRAL_ANGLE = np.pi/2. + CENTER = np.array(3*[0]) + CENTRAL_ANGLE = np.pi/2 ctp = espressomd.math.CylindricalTransformationParameters(center = CENTER, axis = [1.,0.,0.], orientation = [0.,0.,1.]) shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness = 0., length=LENGTH, central_angle = CENTRAL_ANGLE) - probe_pos = CENTER + [0, 0.1, 10] - closest_on_surface = CENTER + [0 , R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.)] + + #point within length + probe_pos = CENTER + [0,sys.float_info.epsilon, 1.234] + closest_on_surface = CENTER + [0, R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.) ] dist = shape.calc_distance(position = probe_pos) d_vec_expected = probe_pos-closest_on_surface self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) - # check central angle with funnel-type frustum + # point outside of length + probe_pos = CENTER + [LENGTH,sys.float_info.epsilon, 1.234] + closest_on_surface = CENTER + [LENGTH/2., R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.) ] + dist = shape.calc_distance(position = probe_pos) + d_vec_expected = probe_pos-closest_on_surface + self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) + # check central angle with funnel-type frustum + shape.r1 = LENGTH + shape.r2 = 0 + shape.central_angle = np.pi + # with this setup, the edges coincide with the xy angle bisectors + probe_pos = CENTER + [5-LENGTH/2.,-sys.float_info.epsilon,sys.float_info.epsilon] + d_vec_expected = 5/2 * np.array([1,1,0]) + dist = shape.calc_distance(position = probe_pos) + #self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) def test_simplepore(self): From 435859f4aaef9c6135bd96e0b5a77253222ca13d Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 17:08:53 +0100 Subject: [PATCH 07/14] adapt visualizer and sample --- samples/visualization_constraints.py | 4 +++- src/python/espressomd/visualization_opengl.py | 19 ++++++++++++------- testsuite/python/constraint_shape_based.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/samples/visualization_constraints.py b/samples/visualization_constraints.py index de0a56ac043..51ec098faff 100644 --- a/samples/visualization_constraints.py +++ b/samples/visualization_constraints.py @@ -22,6 +22,7 @@ import argparse import espressomd +import espressomd.math import espressomd.shapes import espressomd.visualization_opengl @@ -96,9 +97,10 @@ particle_type=0, penetrable=True) elif args.shape == "HollowConicalFrustum": + ctp = espressomd.math.CylindricalTransformationParameters(axis=1/np.sqrt(2)*np.array([0.0,1.0 , 1.0]), center=[25, 25, 25], orientation = [1,0,0]) system.constraints.add(shape=espressomd.shapes.HollowConicalFrustum( r1=12, r2=8, length=15.0, thickness=3, - axis=[0.0, 1.0, 1.0], center=[25, 25, 25], direction=1), + cyl_transform_params = ctp, direction=1, central_angle = np.pi/2), particle_type=0, penetrable=True) elif args.shape == "Torus": diff --git a/src/python/espressomd/visualization_opengl.py b/src/python/espressomd/visualization_opengl.py index 412cd37b50f..74b2f8c623f 100644 --- a/src/python/espressomd/visualization_opengl.py +++ b/src/python/espressomd/visualization_opengl.py @@ -1977,12 +1977,17 @@ def __init__(self, shape, particle_type, color, material, quality, box_l, rasterize_resolution, rasterize_pointsize): super().__init__(shape, particle_type, color, material, quality, box_l, rasterize_resolution, rasterize_pointsize) - self.center = np.array(self.shape.get_parameter('center')) - self.radius_1 = np.array(self.shape.get_parameter('r1')) - self.radius_2 = np.array(self.shape.get_parameter('r2')) - self.length = np.array(self.shape.get_parameter('length')) - self.thickness = np.array(self.shape.get_parameter('thickness')) - self.axis = np.array(self.shape.get_parameter('axis')) + ctp = self.shape.get_parameter('cyl_transform_params') + self.center = np.array(ctp.center) + self.axis = np.array(ctp.axis) + self.orientation = np.array(ctp.orientation) + self.radius_1 = self.shape.get_parameter('r1') + self.radius_2 = self.shape.get_parameter('r2') + self.length = self.shape.get_parameter('length') + self.thickness = self.shape.get_parameter('thickness') + self.central_angle = self.shape.get_parameter('central_angle') + + def draw(self): """ @@ -1990,7 +1995,7 @@ def draw(self): Use rasterization of base class, otherwise. """ - if bool(OpenGL.GLE.gleSpiral): + if bool(OpenGL.GLE.gleSpiral) and self.central_angle==0.: self._draw_using_gle() else: super().draw() diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index b3eb2878371..adcae8f258e 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -152,7 +152,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ probe_pos = CENTER + [5-LENGTH/2.,-sys.float_info.epsilon,sys.float_info.epsilon] d_vec_expected = 5/2 * np.array([1,1,0]) dist = shape.calc_distance(position = probe_pos) - #self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) From 8cc23942bff4fd40bcf46b4c984bc7f02c3c389d Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 17:33:26 +0100 Subject: [PATCH 08/14] doc --- doc/sphinx/constraints.rst | 8 +++++++- ...hape-hollowconicalfrustum_central_angle.png | Bin 0 -> 109875 bytes src/python/espressomd/shapes.py | 13 +++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 doc/sphinx/figures/shape-hollowconicalfrustum_central_angle.png diff --git a/doc/sphinx/constraints.rst b/doc/sphinx/constraints.rst index 4dd274fbf2f..f04c50a0fd0 100644 --- a/doc/sphinx/constraints.rst +++ b/doc/sphinx/constraints.rst @@ -440,7 +440,8 @@ HollowConicalFrustum :class:`espressomd.shapes.HollowConicalFrustum` -A hollow cone with round corners. The specific parameters +A hollow cone with round corners. +Can include an opening in the side (see figures below). The specific parameters are described in the shape's class :class:`espressomd.shapes.HollowConicalFrustum`. .. figure:: figures/shape-conical_frustum.png @@ -448,6 +449,11 @@ are described in the shape's class :class:`espressomd.shapes.HollowConicalFrustu :align: center :height: 6.00000cm +.. figure:: figures/shape-hollowconicalfrustum_central_angle.png + :alt: Visualization a HollowConicalFrustum shape with central angle + :align: center + :height: 6.00000cm + .. figure:: figures/conical_frustum.png :alt: Schematic for the HollowConicalFrustum shape with labeled geometrical parameters. :align: center diff --git a/doc/sphinx/figures/shape-hollowconicalfrustum_central_angle.png b/doc/sphinx/figures/shape-hollowconicalfrustum_central_angle.png new file mode 100644 index 0000000000000000000000000000000000000000..7e2f5ac714f6cd81da18ef3528541c841e1ceb02 GIT binary patch literal 109875 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 Fe*sZ!r=b7< literal 0 HcmV?d00001 diff --git a/src/python/espressomd/shapes.py b/src/python/espressomd/shapes.py index 214a81944ca..cf47a53cf7a 100644 --- a/src/python/espressomd/shapes.py +++ b/src/python/espressomd/shapes.py @@ -231,16 +231,21 @@ class HollowConicalFrustum(Shape, ScriptInterfaceHelper): Attributes ---------- + cyl_transform_params : :class:`espressomd.math.CylindricalTransformationParameters`, + Parameters of the spacial orientation of the frustum. Contained must be parameters for ``center`` and ``axis``. ``orientation`` has no effect, unless ``central_angle != 0`` r1: :obj:`float` Radius r1. r2: :obj:`float` Radius r2. length: :obj:`float` Length of the conical frustum along ``axis``. - axis: (3,) array_like of :obj:`float` - Symmetry axis. - center: (3,) array_like of :obj:`float` - Position of the center. + thickness: float + The thickness of the frustum. Also determines the rounding radius of the edges + direction: :obj:`int`, optional + Surface orientation, for +1 the normal points + out of the mantel, for -1 it points inside of the shape. Defaults to 1 + cantral_angle: :obj:`float`, optional + A ``central_angle`` creates an opening in the frustum along the side, centered symmetrically around the ``direction`` of ``cyl_transform_params``. Must be between ``0`` and ``2 pi``. Defaults to 0. .. image:: figures/conical_frustum.png From 0ff6d31b407b20192aa2e3293b8fa3bac479d5c0 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Thu, 25 Mar 2021 17:40:10 +0100 Subject: [PATCH 09/14] fmt --- samples/visualization_constraints.py | 5 +- src/python/espressomd/math.py | 2 +- src/python/espressomd/visualization_opengl.py | 4 +- .../CylindricalTransformationParameters.hpp | 31 +++-- .../shapes/HollowConicalFrustum.hpp | 37 +++-- .../include/shapes/HollowConicalFrustum.hpp | 24 ++-- src/shapes/src/HollowConicalFrustum.cpp | 64 +++++---- .../unit_tests/HollowConicalFrustum_test.cpp | 7 +- .../cylindrical_transformation_parameters.hpp | 10 +- testsuite/python/constraint_shape_based.py | 131 +++++++++++------- testsuite/python/es_math.py | 36 +++-- 11 files changed, 208 insertions(+), 143 deletions(-) diff --git a/samples/visualization_constraints.py b/samples/visualization_constraints.py index 51ec098faff..020d03611f5 100644 --- a/samples/visualization_constraints.py +++ b/samples/visualization_constraints.py @@ -97,10 +97,11 @@ particle_type=0, penetrable=True) elif args.shape == "HollowConicalFrustum": - ctp = espressomd.math.CylindricalTransformationParameters(axis=1/np.sqrt(2)*np.array([0.0,1.0 , 1.0]), center=[25, 25, 25], orientation = [1,0,0]) + ctp = espressomd.math.CylindricalTransformationParameters( + axis=1 / np.sqrt(2) * np.array([0.0, 1.0, 1.0]), center=[25, 25, 25], orientation=[1, 0, 0]) system.constraints.add(shape=espressomd.shapes.HollowConicalFrustum( r1=12, r2=8, length=15.0, thickness=3, - cyl_transform_params = ctp, direction=1, central_angle = np.pi/2), + cyl_transform_params=ctp, direction=1, central_angle=np.pi / 2), particle_type=0, penetrable=True) elif args.shape == "Torus": diff --git a/src/python/espressomd/math.py b/src/python/espressomd/math.py index 996364c995c..a7c77f287ea 100644 --- a/src/python/espressomd/math.py +++ b/src/python/espressomd/math.py @@ -32,7 +32,7 @@ class CylindricalTransformationParameters(ScriptInterfaceHelper): Orientation vector of the ``z``-axis of the cylindrical coordinate system. orientation: (3,) array_like of :obj:`float`, default = [1, 0, 0] The axis on which ``phi = 0``. - + Notes ----- If you provide no arguments, the defaults above are set. diff --git a/src/python/espressomd/visualization_opengl.py b/src/python/espressomd/visualization_opengl.py index 74b2f8c623f..f37512c2936 100644 --- a/src/python/espressomd/visualization_opengl.py +++ b/src/python/espressomd/visualization_opengl.py @@ -1986,8 +1986,6 @@ def __init__(self, shape, particle_type, color, material, self.length = self.shape.get_parameter('length') self.thickness = self.shape.get_parameter('thickness') self.central_angle = self.shape.get_parameter('central_angle') - - def draw(self): """ @@ -1995,7 +1993,7 @@ def draw(self): Use rasterization of base class, otherwise. """ - if bool(OpenGL.GLE.gleSpiral) and self.central_angle==0.: + if bool(OpenGL.GLE.gleSpiral) and self.central_angle == 0.: self._draw_using_gle() else: super().draw() diff --git a/src/script_interface/CylindricalTransformationParameters.hpp b/src/script_interface/CylindricalTransformationParameters.hpp index 6e60121c9d3..0cd522b2ef2 100644 --- a/src/script_interface/CylindricalTransformationParameters.hpp +++ b/src/script_interface/CylindricalTransformationParameters.hpp @@ -47,22 +47,27 @@ class CylindricalTransformationParameters } void do_construct(VariantMap const ¶ms) override { auto n_params = params.size(); - switch(n_params){ - case 0: m_transform_params = - std::make_shared(); + switch (n_params) { + case 0: + m_transform_params = + std::make_shared(); break; - case 2: m_transform_params = - std::make_shared( - get_value(params, "center"), - get_value(params, "axis")); + case 2: + m_transform_params = + std::make_shared( + get_value(params, "center"), + get_value(params, "axis")); break; - case 3: m_transform_params = - std::make_shared( - get_value(params, "center"), - get_value(params, "axis"), - get_value(params, "orientation")); + case 3: + m_transform_params = + std::make_shared( + get_value(params, "center"), + get_value(params, "axis"), + get_value(params, "orientation")); break; - default: throw std::runtime_error("Provide either no arguments, center and axis, or center and axis and orientation"); + default: + throw std::runtime_error("Provide either no arguments, center and axis, " + "or center and axis and orientation"); } } diff --git a/src/script_interface/shapes/HollowConicalFrustum.hpp b/src/script_interface/shapes/HollowConicalFrustum.hpp index 287151cf4a6..14f17bea716 100644 --- a/src/script_interface/shapes/HollowConicalFrustum.hpp +++ b/src/script_interface/shapes/HollowConicalFrustum.hpp @@ -20,17 +20,17 @@ #ifndef ESPRESSO_HOLLOW_CONICAL_FRUSTUM_HPP #define ESPRESSO_HOLLOW_CONICAL_FRUSTUM_HPP #include "Shape.hpp" -#include #include +#include namespace ScriptInterface { namespace Shapes { class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum(){ - add_parameters({ - {"cyl_transform_params", m_cyl_transform_params}, + HollowConicalFrustum() { + add_parameters( + {{"cyl_transform_params", m_cyl_transform_params}, {"r1", [this](Variant const &v) { m_hollow_conical_frustum->set_r1(get_value(v)); @@ -56,28 +56,27 @@ class HollowConicalFrustum : public Shape { m_hollow_conical_frustum->set_direction(get_value(v)); }, [this]() { return m_hollow_conical_frustum->direction(); }}, - {"central_angle", - [this](Variant const &v) { - m_hollow_conical_frustum->set_central_angle(get_value(v)); - }, - [this]() { return m_hollow_conical_frustum->central_angle(); }}}); + {"central_angle", + [this](Variant const &v) { + m_hollow_conical_frustum->set_central_angle(get_value(v)); + }, + [this]() { return m_hollow_conical_frustum->central_angle(); }}}); } void do_construct(VariantMap const ¶ms) override { set_from_args(m_cyl_transform_params, params, "cyl_transform_params"); if (m_cyl_transform_params) - m_hollow_conical_frustum = std::make_shared<::Shapes::HollowConicalFrustum>( - get_value(params,"r1"), - get_value(params,"r2"), - get_value(params,"length"), - get_value_or(params,"thickness",0.), - get_value_or(params,"direction",1), - get_value_or(params, "central_angle",0.), - m_cyl_transform_params->cyl_transform_params() - - ); + m_hollow_conical_frustum = + std::make_shared<::Shapes::HollowConicalFrustum>( + get_value(params, "r1"), get_value(params, "r2"), + get_value(params, "length"), + get_value_or(params, "thickness", 0.), + get_value_or(params, "direction", 1), + get_value_or(params, "central_angle", 0.), + m_cyl_transform_params->cyl_transform_params() + ); } std::shared_ptr<::Shapes::Shape> shape() const override { diff --git a/src/shapes/include/shapes/HollowConicalFrustum.hpp b/src/shapes/include/shapes/HollowConicalFrustum.hpp index ede3b7853a3..1f6bc91d850 100644 --- a/src/shapes/include/shapes/HollowConicalFrustum.hpp +++ b/src/shapes/include/shapes/HollowConicalFrustum.hpp @@ -21,8 +21,8 @@ #define SHAPES_CONICAL_FRUSTUM_HPP #include "Shape.hpp" -#include #include "utils/math/cylindrical_transformation_parameters.hpp" +#include #include #include @@ -50,16 +50,23 @@ namespace Shapes { */ class HollowConicalFrustum : public Shape { public: - HollowConicalFrustum(double const r1, double const r2, double const length, double const thickness, int const direction, double const central_angle, std::shared_ptr cyl_transform_params) + HollowConicalFrustum( + double const r1, double const r2, double const length, + double const thickness, int const direction, double const central_angle, + std::shared_ptr + cyl_transform_params) : m_r1(r1), m_r2(r2), m_length(length), m_thickness(thickness), - m_direction(direction), m_central_angle(central_angle), m_cyl_transform_params(std::move(cyl_transform_params)) {} + m_direction(direction), m_central_angle(central_angle), + m_cyl_transform_params(std::move(cyl_transform_params)) {} void set_r1(double const radius) { m_r1 = radius; } void set_r2(double const radius) { m_r2 = radius; } void set_length(double const length) { m_length = length; } void set_thickness(double const thickness) { m_thickness = thickness; } void set_direction(int const dir) { m_direction = dir; } - void set_central_angle(double const central_angle) {m_central_angle = central_angle; } + void set_central_angle(double const central_angle) { + m_central_angle = central_angle; + } /// Get radius 1 perpendicular to axis. double radius1() const { return m_r1; } @@ -70,10 +77,9 @@ class HollowConicalFrustum : public Shape { /// Get thickness of the frustum. double thickness() const { return m_thickness; } /// Get direction - int direction() const {return m_direction;} + int direction() const { return m_direction; } /// Get central angle - double central_angle() const {return m_central_angle; } - + double central_angle() const { return m_central_angle; } /** * @brief Calculate the distance vector and its norm between a given position @@ -92,8 +98,8 @@ class HollowConicalFrustum : public Shape { double m_thickness; int m_direction; double m_central_angle; - std::shared_ptr m_cyl_transform_params; - + std::shared_ptr + m_cyl_transform_params; }; } // namespace Shapes diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index dcbc341f00a..f6fef78b6a6 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -34,55 +34,67 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( v, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); - auto project_on_line = [](auto const vec, auto const line_start, auto const line_director) { - return line_start + line_director *((vec-line_start)*line_director); + auto project_on_line = [](auto const vec, auto const line_start, + auto const line_director) { + return line_start + line_director * ((vec - line_start) * line_director); }; Utils::Vector3d pos_closest; - if (Utils::abs(pos_cyl[1])>=m_central_angle/2.){ + if (Utils::abs(pos_cyl[1]) >= m_central_angle / 2.) { // Go to 2d, find the projection onto the cone mantle auto const pos_2d = Utils::Vector2d{{pos_cyl[0], pos_cyl[2]}}; - auto const r1_endpoint = Utils::Vector2d{{m_r1,m_length/2.}}; - auto const r2_endpoint = Utils::Vector2d{{m_r2,-m_length/2.}}; - auto const line_director = (r2_endpoint-r1_endpoint).normalized(); + auto const r1_endpoint = Utils::Vector2d{{m_r1, m_length / 2.}}; + auto const r2_endpoint = Utils::Vector2d{{m_r2, -m_length / 2.}}; + auto const line_director = (r2_endpoint - r1_endpoint).normalized(); auto closest_point_2d = project_on_line(pos_2d, r1_endpoint, line_director); // correct projection if it is outside the frustum - if (Utils::abs(closest_point_2d[1])>m_length/2.){ - bool at_r1 = closest_point_2d[1]>0; + if (Utils::abs(closest_point_2d[1]) > m_length / 2.) { + bool at_r1 = closest_point_2d[1] > 0; closest_point_2d[0] = at_r1 ? m_r1 : m_r2; - closest_point_2d[1] = at_r1 ? m_length/2. : -m_length/2.; + closest_point_2d[1] = at_r1 ? m_length / 2. : -m_length / 2.; } // Go back to cartesian coordinates of the box frame pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( - {closest_point_2d[0], pos_cyl[1], closest_point_2d[1]}, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + + {closest_point_2d[0], pos_cyl[1], closest_point_2d[1]}, + m_cyl_transform_params->axis(), + m_cyl_transform_params->orientation()) + m_cyl_transform_params->center(); - } - else{ - // We cannot go to 2d because the central-angle-gap breaks rotational symmetry, - // so we have to get the line endpoints of the closer edge in 3d cartesian coordinates (but still in the reference frame of the HCF) + } else { + // We cannot go to 2d because the central-angle-gap breaks rotational + // symmetry, so we have to get the line endpoints of the closer edge in 3d + // cartesian coordinates (but still in the reference frame of the HCF) // Cannot use Utils::sgn because of pos_cyl[1]==0 corner case - auto const endpoint_angle = pos_cyl[1]>=0 ? m_central_angle/2. : -m_central_angle/2.; - auto const r1_endpoint = Utils::transform_coordinate_cylinder_to_cartesian(Utils::Vector3d{{m_r1,endpoint_angle,m_length/2.}}); - auto const r2_endpoint = Utils::transform_coordinate_cylinder_to_cartesian(Utils::Vector3d{{m_r2,endpoint_angle,-m_length/2.}}); - auto const line_director = (r2_endpoint-r1_endpoint).normalized(); - auto const pos_hcf_frame = Utils::transform_coordinate_cylinder_to_cartesian(pos_cyl); - auto pos_closest_hcf_frame = project_on_line(pos_hcf_frame, r1_endpoint, line_director); + auto const endpoint_angle = + pos_cyl[1] >= 0 ? m_central_angle / 2. : -m_central_angle / 2.; + auto const r1_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( + Utils::Vector3d{{m_r1, endpoint_angle, m_length / 2.}}); + auto const r2_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( + Utils::Vector3d{{m_r2, endpoint_angle, -m_length / 2.}}); + auto const line_director = (r2_endpoint - r1_endpoint).normalized(); + auto const pos_hcf_frame = + Utils::transform_coordinate_cylinder_to_cartesian(pos_cyl); + auto pos_closest_hcf_frame = + project_on_line(pos_hcf_frame, r1_endpoint, line_director); - // Go back to cylindrical coordinates (HCF reference frame), here we can apply the capping at z = plusminus l/2. - auto pos_closest_hcf_cyl = Utils::transform_coordinate_cartesian_to_cylinder(pos_closest_hcf_frame); - if (Utils::abs(pos_closest_hcf_cyl[2])>m_length/2.){ - bool at_r1 = pos_closest_hcf_cyl[2]>0.; + // Go back to cylindrical coordinates (HCF reference frame), here we can + // apply the capping at z = plusminus l/2. + auto pos_closest_hcf_cyl = + Utils::transform_coordinate_cartesian_to_cylinder( + pos_closest_hcf_frame); + if (Utils::abs(pos_closest_hcf_cyl[2]) > m_length / 2.) { + bool at_r1 = pos_closest_hcf_cyl[2] > 0.; pos_closest_hcf_cyl[0] = at_r1 ? m_r1 : m_r2; - pos_closest_hcf_cyl[2] = at_r1 ? m_length/2. : -m_length/2.; + pos_closest_hcf_cyl[2] = at_r1 ? m_length / 2. : -m_length / 2.; } // Finally, go to cartesian coordinates of the box frame pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( - pos_closest_hcf_cyl, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + + pos_closest_hcf_cyl, m_cyl_transform_params->axis(), + m_cyl_transform_params->orientation()) + m_cyl_transform_params->center(); } diff --git a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp index d2d731f0d3f..843fc719eb8 100644 --- a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp +++ b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp @@ -39,7 +39,7 @@ BOOST_AUTO_TEST_CASE(dist_function) { { auto ctp = std::make_shared(); - Shapes::HollowConicalFrustum c(R1,R2, L, 0.,1,ctp); + Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, ctp); auto pos = Utils::Vector3d{0.0, 0.0, L / 2.0}; Utils::Vector3d vec; @@ -66,8 +66,9 @@ BOOST_AUTO_TEST_CASE(dist_function) { BOOST_CHECK_SMALL(dist - .5, eps); } { - auto ctp = std::make_shared(Utils::Vector3d{{0.,0.,0.}}, Utils::Vector3d{{1.,0.,0.}}); - Shapes::HollowConicalFrustum c(R1,R2, L, 0.,1,ctp); + auto ctp = std::make_shared( + Utils::Vector3d{{0., 0., 0.}}, Utils::Vector3d{{1., 0., 0.}}); + Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, ctp); auto pos = Utils::Vector3d{L / 2.0, 0.0, 0.0}; Utils::Vector3d vec; diff --git a/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp b/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp index 68b7b173c71..e4cbafe3910 100644 --- a/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp +++ b/src/utils/include/utils/math/cylindrical_transformation_parameters.hpp @@ -46,11 +46,13 @@ class CylindricalTransformationParameters { validate(); } /** - * @brief if you only provide center and axis, an orientation will be generated automatically such that it is orthogonal to axis - */ + * @brief if you only provide center and axis, an orientation will be + * generated automatically such that it is orthogonal to axis + */ CylindricalTransformationParameters(Utils::Vector3d const ¢er, - Utils::Vector3d const &axis) - : m_center(center), m_axis(axis), m_orientation(Utils::calc_orthonormal_vector(axis)) {} + Utils::Vector3d const &axis) + : m_center(center), m_axis(axis), + m_orientation(Utils::calc_orthonormal_vector(axis)) {} Utils::Vector3d center() const { return m_center; } Utils::Vector3d axis() const { return m_axis; } diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index adcae8f258e..aadb0342ce2 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -54,12 +54,21 @@ def test_hollow_conical_frustum(self): R2 = 10.0 LENGTH = 15.0 D = 2.4 - + # test attributes - ctp = espressomd.math.CylindricalTransformationParameters(center = 3*[5], axis = [1.,0.,0.]) - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, direction = -1, length=LENGTH, central_angle = np.pi) + ctp = espressomd.math.CylindricalTransformationParameters( + center=3 * [5], axis=[1., 0., 0.]) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, + r1=R1, + r2=R2, + thickness=D, + direction=-1, + length=LENGTH, + central_angle=np.pi) - np.testing.assert_almost_equal(np.copy(shape.cyl_transform_params.center), 3*[5]) + np.testing.assert_almost_equal( + np.copy(shape.cyl_transform_params.center), 3 * [5]) self.assertAlmostEqual(shape.r1, R1) self.assertAlmostEqual(shape.r2, R2) self.assertAlmostEqual(shape.thickness, D) @@ -67,31 +76,39 @@ def test_hollow_conical_frustum(self): self.assertEqual(shape.direction, -1) self.assertAlmostEqual(shape.central_angle, np.pi) - # test points on and inside of the shape ctp = espressomd.math.CylindricalTransformationParameters() - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=0.0, length=LENGTH) - + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, r1=R1, r2=R2, thickness=0.0, length=LENGTH) + def z(y, r1, r2, l): return l / (r1 - r2) * \ y + l / 2. - l * r1 / (r1 - r2) - + y_vals = np.linspace(R1, R2, 100) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], 0.0) - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH, direction=-1) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, + r1=R1, + r2=R2, + thickness=D, + length=LENGTH, + direction=-1) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], 0.5 * D) - - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH) + + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, r1=R1, r2=R2, thickness=D, length=LENGTH) for y in y_vals: dist = shape.calc_distance(position=[0.0, y, z(y, R1, R2, LENGTH)]) self.assertAlmostEqual(dist[0], -0.5 * D) - + # check sign of dist - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness=D, length=LENGTH) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, r1=R1, r2=R1, thickness=D, length=LENGTH) self.assertLess(shape.calc_distance( position=[0.0, R1, 0.25 * LENGTH])[0], 0.0) self.assertLess(shape.calc_distance( @@ -101,7 +118,13 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ self.assertGreater(shape.calc_distance( position=[0.0, R1 - (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness=D, length=LENGTH, direction=-1) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, + r1=R1, + r2=R1, + thickness=D, + length=LENGTH, + direction=-1) self.assertGreater(shape.calc_distance( position=[0.0, R1, 0.25 * LENGTH])[0], 0.0) self.assertGreater(shape.calc_distance( @@ -110,51 +133,63 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ position=[0.0, R1 + (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) self.assertLess(shape.calc_distance( position=[0.0, R1 - (0.5 + sys.float_info.epsilon) * D, 0.25 * LENGTH])[0], 0.0) - + # test points outside of the shape - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R2, thickness=D, length=LENGTH, direction=1) - - dist = shape.calc_distance(position = [R1, 0, LENGTH/2.+5]) - self.assertAlmostEqual(dist[0], 5-D/2.) - np.testing.assert_array_almost_equal(dist[1], [0,0,dist[0]]) - - dist = shape.calc_distance(position = [0.1, 0, LENGTH/2.]) - self.assertAlmostEqual(dist[0], R1-D/2.-0.1) - np.testing.assert_array_almost_equal(dist[1], [-dist[0],0,0]) - + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, r1=R1, r2=R2, thickness=D, length=LENGTH, direction=1) + + dist = shape.calc_distance(position=[R1, 0, LENGTH / 2. + 5]) + self.assertAlmostEqual(dist[0], 5 - D / 2.) + np.testing.assert_array_almost_equal(dist[1], [0, 0, dist[0]]) + + dist = shape.calc_distance(position=[0.1, 0, LENGTH / 2.]) + self.assertAlmostEqual(dist[0], R1 - D / 2. - 0.1) + np.testing.assert_array_almost_equal(dist[1], [-dist[0], 0, 0]) + # check rotated coordinates, central angle with straight frustum - CENTER = np.array(3*[0]) - CENTRAL_ANGLE = np.pi/2 - ctp = espressomd.math.CylindricalTransformationParameters(center = CENTER, axis = [1.,0.,0.], orientation = [0.,0.,1.]) - shape = espressomd.shapes.HollowConicalFrustum(cyl_transform_params = ctp, r1=R1, r2=R1, thickness = 0., length=LENGTH, central_angle = CENTRAL_ANGLE) - - #point within length - probe_pos = CENTER + [0,sys.float_info.epsilon, 1.234] - closest_on_surface = CENTER + [0, R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.) ] - dist = shape.calc_distance(position = probe_pos) - d_vec_expected = probe_pos-closest_on_surface - self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + CENTER = np.array(3 * [0]) + CENTRAL_ANGLE = np.pi / 2 + ctp = espressomd.math.CylindricalTransformationParameters( + center=CENTER, axis=[1., 0., 0.], orientation=[0., 0., 1.]) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, + r1=R1, + r2=R1, + thickness=0., + length=LENGTH, + central_angle=CENTRAL_ANGLE) + + # point within length + probe_pos = CENTER + [0, sys.float_info.epsilon, 1.234] + closest_on_surface = CENTER + [0, + R1 * np.sin(CENTRAL_ANGLE / 2.), + R1 * np.cos(CENTRAL_ANGLE / 2.)] + dist = shape.calc_distance(position=probe_pos) + d_vec_expected = probe_pos - closest_on_surface + self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) - + # point outside of length - probe_pos = CENTER + [LENGTH,sys.float_info.epsilon, 1.234] - closest_on_surface = CENTER + [LENGTH/2., R1 *np.sin(CENTRAL_ANGLE/2.), R1 *np.cos(CENTRAL_ANGLE/2.) ] - dist = shape.calc_distance(position = probe_pos) - d_vec_expected = probe_pos-closest_on_surface - self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + probe_pos = CENTER + [LENGTH, sys.float_info.epsilon, 1.234] + closest_on_surface = CENTER + [LENGTH / 2., + R1 * np.sin(CENTRAL_ANGLE / 2.), + R1 * np.cos(CENTRAL_ANGLE / 2.)] + dist = shape.calc_distance(position=probe_pos) + d_vec_expected = probe_pos - closest_on_surface + self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) - + # check central angle with funnel-type frustum shape.r1 = LENGTH shape.r2 = 0 shape.central_angle = np.pi # with this setup, the edges coincide with the xy angle bisectors - probe_pos = CENTER + [5-LENGTH/2.,-sys.float_info.epsilon,sys.float_info.epsilon] - d_vec_expected = 5/2 * np.array([1,1,0]) - dist = shape.calc_distance(position = probe_pos) - self.assertAlmostEqual(dist[0],np.linalg.norm(d_vec_expected)) + probe_pos = CENTER + [5 - LENGTH / 2., - + sys.float_info.epsilon, sys.float_info.epsilon] + d_vec_expected = 5 / 2 * np.array([1, 1, 0]) + dist = shape.calc_distance(position=probe_pos) + self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) - def test_simplepore(self): """ diff --git a/testsuite/python/es_math.py b/testsuite/python/es_math.py index 32660025679..16782fbfc6f 100644 --- a/testsuite/python/es_math.py +++ b/testsuite/python/es_math.py @@ -18,31 +18,37 @@ import unittest as ut import espressomd.math + class TestMath(ut.TestCase): - - def check_orthonormality(self,vec1, vec2): - self.assertAlmostEqual(np.linalg.norm(vec1),1.) - self.assertAlmostEqual(np.linalg.norm(vec2),1.) + + def check_orthonormality(self, vec1, vec2): + self.assertAlmostEqual(np.linalg.norm(vec1), 1.) + self.assertAlmostEqual(np.linalg.norm(vec2), 1.) self.assertAlmostEqual(np.dot(vec1, vec2), 0) - + def test_cylindrical_transformation_parameters(self): - """ Test for the varous constructors of CylindricalTransformationParameters """ - + ctp_default = espressomd.math.CylindricalTransformationParameters() self.check_orthonormality(ctp_default.axis, ctp_default.orientation) - + axis = np.array([-17, 0.1, np.pi]) axis /= np.linalg.norm(axis) - ctp_auto_orientation = espressomd.math.CylindricalTransformationParameters(center = 3*[42], axis = axis) - self.check_orthonormality(ctp_auto_orientation.axis, ctp_auto_orientation.orientation) - - ctp_full = espressomd.math.CylindricalTransformationParameters(center = 3*[42], axis = [0,1,0], orientation = [1,0,0]) + ctp_auto_orientation = espressomd.math.CylindricalTransformationParameters( + center=3 * [42], axis=axis) + self.check_orthonormality( + ctp_auto_orientation.axis, + ctp_auto_orientation.orientation) + + ctp_full = espressomd.math.CylindricalTransformationParameters( + center=3 * [42], axis=[0, 1, 0], orientation=[1, 0, 0]) self.check_orthonormality(ctp_full.axis, ctp_full.orientation) - + with self.assertRaises(Exception): - ctp_only_center = espressomd.math.CylindricalTransformationParameters(center = 3*[42]) + ctp_only_center = espressomd.math.CylindricalTransformationParameters( + center=3 * [42]) + ctp_only_center.axis = 3 * [3] + if __name__ == "__main__": ut.main() - From ec14252658199e8fedb7ffdc193ca007b9f6c242 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Fri, 26 Mar 2021 12:28:23 +0100 Subject: [PATCH 10/14] HCF unit test central angle argument --- src/shapes/unit_tests/HollowConicalFrustum_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp index 843fc719eb8..618e848b973 100644 --- a/src/shapes/unit_tests/HollowConicalFrustum_test.cpp +++ b/src/shapes/unit_tests/HollowConicalFrustum_test.cpp @@ -39,7 +39,7 @@ BOOST_AUTO_TEST_CASE(dist_function) { { auto ctp = std::make_shared(); - Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, ctp); + Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, 0., ctp); auto pos = Utils::Vector3d{0.0, 0.0, L / 2.0}; Utils::Vector3d vec; @@ -68,7 +68,7 @@ BOOST_AUTO_TEST_CASE(dist_function) { { auto ctp = std::make_shared( Utils::Vector3d{{0., 0., 0.}}, Utils::Vector3d{{1., 0., 0.}}); - Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, ctp); + Shapes::HollowConicalFrustum c(R1, R2, L, 0., 1, 0., ctp); auto pos = Utils::Vector3d{L / 2.0, 0.0, 0.0}; Utils::Vector3d vec; From 3484016eb3d38a193d1109c1969d15b26519bed0 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Fri, 26 Mar 2021 17:39:44 +0100 Subject: [PATCH 11/14] fix HCF in active matter tutorial --- .../active_matter/exercises/rectification_geometry.py | 9 ++++++--- .../active_matter/exercises/rectification_simulation.py | 2 ++ .../active_matter/solutions/rectification_geometry.py | 8 ++++++-- .../active_matter/solutions/rectification_simulation.py | 8 ++++++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/doc/tutorials/active_matter/exercises/rectification_geometry.py b/doc/tutorials/active_matter/exercises/rectification_geometry.py index 59b74e19bdf..b8eb27856c0 100644 --- a/doc/tutorials/active_matter/exercises/rectification_geometry.py +++ b/doc/tutorials/active_matter/exercises/rectification_geometry.py @@ -30,7 +30,7 @@ from espressomd import lb from espressomd.lbboundaries import LBBoundary import espressomd.shapes - +import espressomd.math # Setup constants @@ -90,9 +90,12 @@ ORAD = (DIAMETER - IRAD) / np.sin(ANGLE) SHIFT = 0.25 * ORAD * np.cos(ANGLE) +ctp = espressomd.math.CylindricalTransformationParameters( + axis=[-1, 0, 0], center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0]) + hollow_cone = LBBoundary(shape=espressomd.shapes.HollowConicalFrustum( - center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0], - axis=[-1, 0, 0], r1=ORAD, r2=IRAD, thickness=2.0, length=18, + cyl_transform_params=ctp, + r1=ORAD, r2=IRAD, thickness=2.0, length=18, direction=1)) system.lbboundaries.add(hollow_cone) diff --git a/doc/tutorials/active_matter/exercises/rectification_simulation.py b/doc/tutorials/active_matter/exercises/rectification_simulation.py index 9432a4928cc..9bdcb55eda2 100644 --- a/doc/tutorials/active_matter/exercises/rectification_simulation.py +++ b/doc/tutorials/active_matter/exercises/rectification_simulation.py @@ -29,6 +29,7 @@ import espressomd from espressomd import assert_features import espressomd.shapes +import espressomd.math assert_features(["ENGINE", "LENNARD_JONES", "ROTATION", "MASS"]) @@ -119,6 +120,7 @@ def a2quat(phi, theta): system.constraints.add(shape=wall, particle_type=1) # Setup cone +ctp = espressomd.math.CylindricalTransformationParameters(...) hollow_cone = espressomd.shapes.HollowConicalFrustum(...) system.constraints.add(shape=hollow_cone, particle_type=1) diff --git a/doc/tutorials/active_matter/solutions/rectification_geometry.py b/doc/tutorials/active_matter/solutions/rectification_geometry.py index e4dff78f5dc..9987ec61001 100644 --- a/doc/tutorials/active_matter/solutions/rectification_geometry.py +++ b/doc/tutorials/active_matter/solutions/rectification_geometry.py @@ -30,6 +30,7 @@ from espressomd import lb from espressomd.lbboundaries import LBBoundary import espressomd.shapes +import espressomd.math # Setup constants @@ -91,9 +92,12 @@ ORAD = (DIAMETER - IRAD) / np.sin(ANGLE) SHIFT = 0.25 * ORAD * np.cos(ANGLE) +ctp = espressomd.math.CylindricalTransformationParameters( + axis=[-1, 0, 0], center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0]) + hollow_cone = LBBoundary(shape=espressomd.shapes.HollowConicalFrustum( - center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0], - axis=[-1, 0, 0], r1=ORAD, r2=IRAD, thickness=2.0, length=18, + cyl_transform_params=ctp, + r1=ORAD, r2=IRAD, thickness=2.0, length=18, direction=1)) system.lbboundaries.add(hollow_cone) diff --git a/doc/tutorials/active_matter/solutions/rectification_simulation.py b/doc/tutorials/active_matter/solutions/rectification_simulation.py index ac1cdab2622..108d932531f 100644 --- a/doc/tutorials/active_matter/solutions/rectification_simulation.py +++ b/doc/tutorials/active_matter/solutions/rectification_simulation.py @@ -29,6 +29,7 @@ import espressomd from espressomd import assert_features import espressomd.shapes +import espressomd.math assert_features(["ENGINE", "LENNARD_JONES", "ROTATION", "MASS"]) @@ -118,9 +119,12 @@ def a2quat(phi, theta): ORAD = (DIAMETER - IRAD) / np.sin(ANGLE) SHIFT = 0.25 * ORAD * np.cos(ANGLE) +ctp = espressomd.math.CylindricalTransformationParameters( + axis=[-1, 0, 0], center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0]) + hollow_cone = espressomd.shapes.HollowConicalFrustum( - center=[BOX_L[0] / 2.0 - 1.3 * SHIFT, BOX_L[1] / 2.0, BOX_L[2] / 2.0], - axis=[-1, 0, 0], r1=ORAD, r2=IRAD, thickness=2.0, length=18, + cyl_transform_params=ctp, + r1=ORAD, r2=IRAD, thickness=2.0, length=18, direction=1) system.constraints.add(shape=hollow_cone, particle_type=1) From 9a36e26b3d3f2c1ae9dc22f454cea315c67899f4 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Tue, 6 Apr 2021 17:01:24 +0200 Subject: [PATCH 12/14] better tests for HCF --- testsuite/python/constraint_shape_based.py | 33 ++++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/testsuite/python/constraint_shape_based.py b/testsuite/python/constraint_shape_based.py index aadb0342ce2..09aa2b0a9bf 100644 --- a/testsuite/python/constraint_shape_based.py +++ b/testsuite/python/constraint_shape_based.py @@ -147,7 +147,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ np.testing.assert_array_almost_equal(dist[1], [-dist[0], 0, 0]) # check rotated coordinates, central angle with straight frustum - CENTER = np.array(3 * [0]) + CENTER = np.array(3 * [5]) CENTRAL_ANGLE = np.pi / 2 ctp = espressomd.math.CylindricalTransformationParameters( center=CENTER, axis=[1., 0., 0.], orientation=[0., 0., 1.]) @@ -160,7 +160,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ central_angle=CENTRAL_ANGLE) # point within length - probe_pos = CENTER + [0, sys.float_info.epsilon, 1.234] + probe_pos = CENTER + [0, 10 * sys.float_info.epsilon, 1.234] closest_on_surface = CENTER + [0, R1 * np.sin(CENTRAL_ANGLE / 2.), R1 * np.cos(CENTRAL_ANGLE / 2.)] @@ -170,7 +170,7 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) # point outside of length - probe_pos = CENTER + [LENGTH, sys.float_info.epsilon, 1.234] + probe_pos = CENTER + [LENGTH, 10 * sys.float_info.epsilon, 1.234] closest_on_surface = CENTER + [LENGTH / 2., R1 * np.sin(CENTRAL_ANGLE / 2.), R1 * np.cos(CENTRAL_ANGLE / 2.)] @@ -180,13 +180,28 @@ def z(y, r1, r2, l): return l / (r1 - r2) * \ np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) # check central angle with funnel-type frustum - shape.r1 = LENGTH - shape.r2 = 0 - shape.central_angle = np.pi + ctp = espressomd.math.CylindricalTransformationParameters( + center=[LENGTH / 2., 0, 0], axis=[1., 0., 0.], orientation=[0., 0., 1.]) + shape = espressomd.shapes.HollowConicalFrustum( + cyl_transform_params=ctp, + r1=LENGTH, + r2=0, + thickness=0., + length=LENGTH, + central_angle=np.pi) # with this setup, the edges coincide with the xy angle bisectors - probe_pos = CENTER + [5 - LENGTH / 2., - - sys.float_info.epsilon, sys.float_info.epsilon] - d_vec_expected = 5 / 2 * np.array([1, 1, 0]) + + # point inside LENGTH + probe_pos = [LENGTH / 2., LENGTH / 2., 5] + d_vec_expected = np.array([0, 0, 5]) + dist = shape.calc_distance(position=probe_pos) + self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected)) + np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) + + # point outside LENGTH + probe_pos = [2 * LENGTH, 5 * LENGTH, 5] + frustum_end = np.array([LENGTH, LENGTH, 0]) + d_vec_expected = probe_pos - frustum_end dist = shape.calc_distance(position=probe_pos) self.assertAlmostEqual(dist[0], np.linalg.norm(d_vec_expected)) np.testing.assert_array_almost_equal(d_vec_expected, np.copy(dist[1])) From 341acf0c2607ed7c457ec5cfef74493417c86e23 Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Tue, 6 Apr 2021 17:01:56 +0200 Subject: [PATCH 13/14] more comments and easier calculation for HCF --- src/shapes/src/HollowConicalFrustum.cpp | 79 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index f6fef78b6a6..551cdf06155 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -28,8 +28,8 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, double &dist, Utils::Vector3d &vec) const { - // transform given position to cylindrical coordinates in the reference frame - // of the cone + // Use the rotational symmetry of the cone: Transformation of pos to the + // cylindrical coordinates in the frame of the cone. auto const v = pos - m_cyl_transform_params->center(); auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( v, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); @@ -39,33 +39,59 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, return line_start + line_director * ((vec - line_start) * line_director); }; + // The point on the frustum closest to pos will be determined, dist and vec + // follow trivially. Utils::Vector3d pos_closest; + if (Utils::abs(pos_cyl[1]) >= m_central_angle / 2.) { - // Go to 2d, find the projection onto the cone mantle + /* First case: pos is not in the gap region defined by central_angle. + * Here, the problem reduces to 2D, because pos_closest lies in the plane + * defined by axis and pos as shown in the figure below: + * + * r1 + * * ----->X r1_endpoint + * ^ a \ + * | x \ + * | i \ + * | s \ *pos + * X center \ + * | \ + * | X pos_closest_2d (to be determined) + * | \ + * | r2 \ + * ----------------->X r2_endpoint + * + * pos_closest_2d is the projection of pos on the line defined by + * r1_endpoint and r2_endpoint. The real pos_closest is then calculated + * using the phi-coordinate of pos_cyl and transformation back to the box + * frame. + */ auto const pos_2d = Utils::Vector2d{{pos_cyl[0], pos_cyl[2]}}; auto const r1_endpoint = Utils::Vector2d{{m_r1, m_length / 2.}}; auto const r2_endpoint = Utils::Vector2d{{m_r2, -m_length / 2.}}; auto const line_director = (r2_endpoint - r1_endpoint).normalized(); - auto closest_point_2d = project_on_line(pos_2d, r1_endpoint, line_director); + auto pos_closest_2d = project_on_line(pos_2d, r1_endpoint, line_director); - // correct projection if it is outside the frustum - if (Utils::abs(closest_point_2d[1]) > m_length / 2.) { - bool at_r1 = closest_point_2d[1] > 0; - closest_point_2d[0] = at_r1 ? m_r1 : m_r2; - closest_point_2d[1] = at_r1 ? m_length / 2. : -m_length / 2.; + // If the projection is outside of the frustum, pos_closest_2d is one of the + // endpoints + if (Utils::abs(pos_closest_2d[1]) > m_length / 2.) { + pos_closest_2d = pos_closest_2d[1] > 0 ? r1_endpoint : r2_endpoint; } - // Go back to cartesian coordinates of the box frame pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( - {closest_point_2d[0], pos_cyl[1], closest_point_2d[1]}, + {pos_closest_2d[0], pos_cyl[1], pos_closest_2d[1]}, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()) + m_cyl_transform_params->center(); } else { - // We cannot go to 2d because the central-angle-gap breaks rotational - // symmetry, so we have to get the line endpoints of the closer edge in 3d - // cartesian coordinates (but still in the reference frame of the HCF) + /* If pos is in the gap region, we cannot go to 2d or cylindrical + * coordinates, because the central-angle-gap breaks rotational symmetry. + * Instead, we parametrise the closer edge of the frustum cutout by its + * endpoints in 3D cartesian coordinates, but still in the reference frame + * of the HCF. The projection onto this edge to find pos_closest is then the + * same procedure as in the previous case. + */ // Cannot use Utils::sgn because of pos_cyl[1]==0 corner case auto const endpoint_angle = @@ -80,22 +106,19 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, auto pos_closest_hcf_frame = project_on_line(pos_hcf_frame, r1_endpoint, line_director); - // Go back to cylindrical coordinates (HCF reference frame), here we can - // apply the capping at z = plusminus l/2. - auto pos_closest_hcf_cyl = - Utils::transform_coordinate_cartesian_to_cylinder( - pos_closest_hcf_frame); - if (Utils::abs(pos_closest_hcf_cyl[2]) > m_length / 2.) { - bool at_r1 = pos_closest_hcf_cyl[2] > 0.; - pos_closest_hcf_cyl[0] = at_r1 ? m_r1 : m_r2; - pos_closest_hcf_cyl[2] = at_r1 ? m_length / 2. : -m_length / 2.; + if (Utils::abs(pos_closest_hcf_frame[2]) > m_length / 2.) { + pos_closest_hcf_frame = + pos_closest_hcf_frame[2] > 0. ? r1_endpoint : r2_endpoint; } - // Finally, go to cartesian coordinates of the box frame - pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( - pos_closest_hcf_cyl, m_cyl_transform_params->axis(), - m_cyl_transform_params->orientation()) + - m_cyl_transform_params->center(); + // Now we transform pos_closest_hcf_frame back to the box frame. + auto const hcf_frame_y_axis = Utils::vector_product( + m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); + pos_closest = + Utils::basis_change(m_cyl_transform_params->orientation(), + hcf_frame_y_axis, m_cyl_transform_params->axis(), + pos_closest_hcf_frame, true) + + m_cyl_transform_params->center(); } auto const u = (pos - pos_closest).normalize(); From 49932c5a476cb0dffa3c5c0c043172217c32d26d Mon Sep 17 00:00:00 2001 From: Christoph Lohrmann Date: Tue, 6 Apr 2021 17:49:27 +0200 Subject: [PATCH 14/14] simplify HCF further, unite gap/non-gap calculation --- src/shapes/src/HollowConicalFrustum.cpp | 159 +++++++++++------------- 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/src/shapes/src/HollowConicalFrustum.cpp b/src/shapes/src/HollowConicalFrustum.cpp index 551cdf06155..041899e2598 100644 --- a/src/shapes/src/HollowConicalFrustum.cpp +++ b/src/shapes/src/HollowConicalFrustum.cpp @@ -28,102 +28,89 @@ void HollowConicalFrustum::calculate_dist(const Utils::Vector3d &pos, double &dist, Utils::Vector3d &vec) const { - // Use the rotational symmetry of the cone: Transformation of pos to the - // cylindrical coordinates in the frame of the cone. - auto const v = pos - m_cyl_transform_params->center(); - auto const pos_cyl = Utils::transform_coordinate_cartesian_to_cylinder( - v, m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); + /* Transform pos to the frame of the frustum (origin = center, z = axis, x = + * orientation, y = cross(z,x) ). Get the angle relative to orientation to + * determine whether pos is in the gap defined by m_central_angle or not. + */ + auto const hcf_frame_y_axis = Utils::vector_product( + m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); + auto const pos_hcf_frame = Utils::basis_change( + m_cyl_transform_params->orientation(), hcf_frame_y_axis, + m_cyl_transform_params->axis(), pos - m_cyl_transform_params->center()); + auto const pos_phi = + Utils::transform_coordinate_cartesian_to_cylinder(pos_hcf_frame)[1]; + + /* The closest point of the frustum to pos will be determined by projection + * onto a line. Which line, depends on where pos is relative to the frustum + * gap. If pos is not within the gap region of central_angle, the closest + * point will be in the plane defined by axis and pos_cyl, as shown in the + * figure below: + * + * r1 + * * ----->X r1_endpoint + * ^ a \ + * | x \ + * | i \ + * | s \ *pos_hcf_frame + * X center \ + * | \ + * | X pos_closest (to be determined) + * | \ + * | r2 \ + * ----------------->X r2_endpoint + * + * In this case, the line is the intersection between the frustum and the + * plane of interest. The endpoints of the line are determined by the radii of + * the frustum, its length and the phi-coordinate of the plane. + * + * If pos is in the gap region, the projection must be made onto the closest + * edge (imagine pos is out-of-plane in the figure). In this case, for the + * endpoints we do not use the phi-coordinate of pos_cyl, but instead the + * phi-coordinate of the closer gap edge. + */ + + auto endpoint_angle = pos_phi; + if (Utils::abs(pos_phi) < m_central_angle / 2.) { + // Cannot use Utils::sgn because of pos_phi==0 corner case + endpoint_angle = + pos_phi > 0. ? m_central_angle / 2. : -m_central_angle / 2.; + } + + auto const r1_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( + Utils::Vector3d{{m_r1, endpoint_angle, m_length / 2.}}); + auto const r2_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( + Utils::Vector3d{{m_r2, endpoint_angle, -m_length / 2.}}); + auto const line_director = (r2_endpoint - r1_endpoint).normalized(); auto project_on_line = [](auto const vec, auto const line_start, auto const line_director) { return line_start + line_director * ((vec - line_start) * line_director); }; - // The point on the frustum closest to pos will be determined, dist and vec - // follow trivially. - Utils::Vector3d pos_closest; - - if (Utils::abs(pos_cyl[1]) >= m_central_angle / 2.) { - /* First case: pos is not in the gap region defined by central_angle. - * Here, the problem reduces to 2D, because pos_closest lies in the plane - * defined by axis and pos as shown in the figure below: - * - * r1 - * * ----->X r1_endpoint - * ^ a \ - * | x \ - * | i \ - * | s \ *pos - * X center \ - * | \ - * | X pos_closest_2d (to be determined) - * | \ - * | r2 \ - * ----------------->X r2_endpoint - * - * pos_closest_2d is the projection of pos on the line defined by - * r1_endpoint and r2_endpoint. The real pos_closest is then calculated - * using the phi-coordinate of pos_cyl and transformation back to the box - * frame. - */ - auto const pos_2d = Utils::Vector2d{{pos_cyl[0], pos_cyl[2]}}; - auto const r1_endpoint = Utils::Vector2d{{m_r1, m_length / 2.}}; - auto const r2_endpoint = Utils::Vector2d{{m_r2, -m_length / 2.}}; - auto const line_director = (r2_endpoint - r1_endpoint).normalized(); - auto pos_closest_2d = project_on_line(pos_2d, r1_endpoint, line_director); - - // If the projection is outside of the frustum, pos_closest_2d is one of the - // endpoints - if (Utils::abs(pos_closest_2d[1]) > m_length / 2.) { - pos_closest_2d = pos_closest_2d[1] > 0 ? r1_endpoint : r2_endpoint; - } + auto pos_closest_hcf_frame = + project_on_line(pos_hcf_frame, r1_endpoint, line_director); - pos_closest = Utils::transform_coordinate_cylinder_to_cartesian( - {pos_closest_2d[0], pos_cyl[1], pos_closest_2d[1]}, - m_cyl_transform_params->axis(), - m_cyl_transform_params->orientation()) + - m_cyl_transform_params->center(); - - } else { - /* If pos is in the gap region, we cannot go to 2d or cylindrical - * coordinates, because the central-angle-gap breaks rotational symmetry. - * Instead, we parametrise the closer edge of the frustum cutout by its - * endpoints in 3D cartesian coordinates, but still in the reference frame - * of the HCF. The projection onto this edge to find pos_closest is then the - * same procedure as in the previous case. - */ + /* It can be that the projection onto the (infinite) line is outside the + * frustum. In that case, the closest point is actually one of the endpoints. + */ + if (Utils::abs(pos_closest_hcf_frame[2]) > m_length / 2.) { + pos_closest_hcf_frame = + pos_closest_hcf_frame[2] > 0. ? r1_endpoint : r2_endpoint; + } - // Cannot use Utils::sgn because of pos_cyl[1]==0 corner case - auto const endpoint_angle = - pos_cyl[1] >= 0 ? m_central_angle / 2. : -m_central_angle / 2.; - auto const r1_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( - Utils::Vector3d{{m_r1, endpoint_angle, m_length / 2.}}); - auto const r2_endpoint = Utils::transform_coordinate_cylinder_to_cartesian( - Utils::Vector3d{{m_r2, endpoint_angle, -m_length / 2.}}); - auto const line_director = (r2_endpoint - r1_endpoint).normalized(); - auto const pos_hcf_frame = - Utils::transform_coordinate_cylinder_to_cartesian(pos_cyl); - auto pos_closest_hcf_frame = - project_on_line(pos_hcf_frame, r1_endpoint, line_director); + // calculate distance and distance vector respecting thickness and direction + auto const u = (pos_hcf_frame - pos_closest_hcf_frame).normalize(); + auto const d = + (pos_hcf_frame - pos_closest_hcf_frame).norm() - 0.5 * m_thickness; - if (Utils::abs(pos_closest_hcf_frame[2]) > m_length / 2.) { - pos_closest_hcf_frame = - pos_closest_hcf_frame[2] > 0. ? r1_endpoint : r2_endpoint; - } + // dist does not depend on reference frame, it can be calculated in the hcf + // frame. + dist = d * m_direction; - // Now we transform pos_closest_hcf_frame back to the box frame. - auto const hcf_frame_y_axis = Utils::vector_product( - m_cyl_transform_params->axis(), m_cyl_transform_params->orientation()); - pos_closest = - Utils::basis_change(m_cyl_transform_params->orientation(), + // vec must be rotated back to the original frame + auto const vec_hcf_frame = d * u; + vec = Utils::basis_change(m_cyl_transform_params->orientation(), hcf_frame_y_axis, m_cyl_transform_params->axis(), - pos_closest_hcf_frame, true) + - m_cyl_transform_params->center(); - } - - auto const u = (pos - pos_closest).normalize(); - auto const d = (pos - pos_closest).norm() - 0.5 * m_thickness; - dist = d * m_direction; - vec = d * u; + vec_hcf_frame, true); } } // namespace Shapes