From 387ddfac098d47200fa9a3a0d6e071e78ee5997a Mon Sep 17 00:00:00 2001
From: "Joachim Wuttke (h)" <j.wuttke@fz-juelich.de>
Date: Wed, 1 Dec 2021 17:39:55 +0100
Subject: [PATCH] temporarily reintegrate libformfactor in BornAgain

---
 Base/CMakeLists.txt               |   2 +-
 CMakeLists.txt                    |   2 +
 cmake/BornAgain/Dependences.cmake |   4 +-
 ff/CMakeLists.txt                 |  33 +++
 ff/Factorial.h                    |  59 +++++
 ff/PolyhedralComponents.cpp       | 397 ++++++++++++++++++++++++++++++
 ff/PolyhedralComponents.h         | 103 ++++++++
 ff/PolyhedralTopology.h           |  44 ++++
 ff/Polyhedron.cpp                 | 192 +++++++++++++++
 ff/Polyhedron.h                   |  64 +++++
 10 files changed, 897 insertions(+), 3 deletions(-)
 create mode 100644 ff/CMakeLists.txt
 create mode 100644 ff/Factorial.h
 create mode 100644 ff/PolyhedralComponents.cpp
 create mode 100644 ff/PolyhedralComponents.h
 create mode 100644 ff/PolyhedralTopology.h
 create mode 100644 ff/Polyhedron.cpp
 create mode 100644 ff/Polyhedron.h

diff --git a/Base/CMakeLists.txt b/Base/CMakeLists.txt
index 9cded5346ed..e9c83e0b799 100644
--- a/Base/CMakeLists.txt
+++ b/Base/CMakeLists.txt
@@ -22,7 +22,7 @@ target_link_libraries(${lib}
     PUBLIC
     ${GSL_LIBRARIES}
     ${FFTW3_LIBRARIES}
-    formfactor
+    formfactor0
     )
 target_include_directories(${lib}
     PUBLIC
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 10289ce2af6..89606bb2f13 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -150,6 +150,8 @@ add_subdirectory(Fit)
 add_subdirectory(Fit/Test/Unit)
 add_subdirectory(Fit/Test/Functional)
 
+# TEMPORARY formfactor
+add_subdirectory(ff)
 
 ## recurse into the given subdirectories
 
diff --git a/cmake/BornAgain/Dependences.cmake b/cmake/BornAgain/Dependences.cmake
index 583d35375c7..02f7790b4f4 100644
--- a/cmake/BornAgain/Dependences.cmake
+++ b/cmake/BornAgain/Dependences.cmake
@@ -8,8 +8,8 @@ find_package(LibHeinz REQUIRED)
 message(STATUS "LibHeinz: found=${LibHeinz_FOUND}, include_dirs=${LibHeinz_INCLUDE_DIR}, "
     "version=${LibHeinz_VERSION}")
 
-find_package(formfactor REQUIRED)
-message(STATUS "formfactor: found=${formfactor_FOUND}, version=${formfactor_VERSION}")
+#find_package(formfactor REQUIRED)
+#message(STATUS "formfactor: found=${formfactor_FOUND}, version=${formfactor_VERSION}")
 
 find_package(Threads REQUIRED)
 find_package(FFTW3 REQUIRED)
diff --git a/ff/CMakeLists.txt b/ff/CMakeLists.txt
new file mode 100644
index 00000000000..8ed490a99b5
--- /dev/null
+++ b/ff/CMakeLists.txt
@@ -0,0 +1,33 @@
+set(lib formfactor0)
+set(${lib}_LIBRARY ${lib} PARENT_SCOPE)
+
+file(GLOB src_files *.cpp)
+set(api_files Polyhedron.h PolyhedralTopology.h PolyhedralComponents.h)
+
+add_library(${lib} ${src_files})
+
+target_include_directories(${lib}
+    PUBLIC
+    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}>
+    $<INSTALL_INTERFACE:include>
+    )
+target_include_directories(${lib} PRIVATE ${LibHeinz_INCLUDE_DIR})
+
+set_target_properties(
+    ${lib} PROPERTIES
+    OUTPUT_NAME ${lib}
+    VERSION ${formfactor_VERSION}
+    SOVERSION ${formfactor_VERSION_MAJOR})
+
+install(
+    TARGETS ${lib}
+    EXPORT formfactorTargets
+    LIBRARY DESTINATION lib
+    RUNTIME DESTINATION lib
+    ARCHIVE DESTINATION lib
+    COMPONENT Libraries)
+
+install(
+    FILES ${api_files}
+    DESTINATION include/ff
+    COMPONENT Headers)
diff --git a/ff/Factorial.h b/ff/Factorial.h
new file mode 100644
index 00000000000..19d41493f18
--- /dev/null
+++ b/ff/Factorial.h
@@ -0,0 +1,59 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/Factorial.h
+//! @brief     Precomputes 1/n!
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+#ifdef SWIG
+#error no need to expose this header to Swig
+#endif
+
+#ifndef USER_API
+#ifndef LIBFORMFACTOR_LIB_FACTORIAL_H
+#define LIBFORMFACTOR_LIB_FACTORIAL_H
+
+#include <array>
+#include <cstddef>
+#include <utility>
+
+namespace {
+
+template <size_t N> struct ReciprocalFactorial {
+    static constexpr double value = ReciprocalFactorial<N - 1>::value / N;
+};
+
+template <> struct ReciprocalFactorial<0> {
+    static constexpr double value = 1.0;
+};
+
+template <template <size_t> class F, size_t... I>
+constexpr std::array<double, sizeof...(I)> generateArrayHelper(std::index_sequence<I...>)
+{
+    return {F<I>::value...};
+}
+
+} // namespace
+
+
+namespace ff_aux {
+
+//! Returns a compile-time generated std::array of reciprocal factorials.
+
+template <size_t N, typename Indices = std::make_index_sequence<N>>
+constexpr std::array<double, N> generateReciprocalFactorialArray()
+{
+    return generateArrayHelper<ReciprocalFactorial>(Indices{});
+}
+
+} // namespace ff_aux
+
+#endif // LIBFORMFACTOR_LIB_FACTORIAL_H
+#endif // USER_API
diff --git a/ff/PolyhedralComponents.cpp b/ff/PolyhedralComponents.cpp
new file mode 100644
index 00000000000..11a4eae2d7f
--- /dev/null
+++ b/ff/PolyhedralComponents.cpp
@@ -0,0 +1,397 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/PolyhedralComponents.cpp
+//! @brief     Implements classes PolyhedralEdge, PolyhedralFace
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+#include "ff/PolyhedralComponents.h"
+#include "ff/Factorial.h"
+
+#include <iomanip>
+#include <stdexcept>
+
+namespace {
+
+const double eps = 2e-16;
+constexpr auto ReciprocalFactorialArray = ff_aux::generateReciprocalFactorialArray<171>();
+
+complex_t sinc(const complex_t z) // cardinal sine function, sin(x)/x
+{
+    // This is an exception from the rule that we must not test floating-point numbers for equality.
+    // For small non-zero arguments, sin(z) returns quite accurately z or z-z^3/6.
+    // There is no loss of precision in computing sin(z)/z.
+    // Therefore there is no need for an expensive test like abs(z)<eps.
+    if (z == complex_t(0., 0.))
+        return 1.0;
+    return std::sin(z) / z;
+}
+
+} // namespace
+
+
+#ifdef ALGORITHM_DIAGNOSTIC
+void ff::PolyhedralDiagnosis::reset()
+{
+    order = 0;
+    algo = 0;
+    msg.clear();
+};
+std::string ff::PolyhedralDiagnosis::message() const
+{
+    std::string ret = "algo=" + std::to_string(algo) + ", order=" + std::to_string(order);
+    if (!msg.empty())
+        ret += ", msg:\n" + msg;
+    return ret;
+}
+bool ff::PolyhedralDiagnosis::operator==(const PolyhedralDiagnosis& other) const
+{
+    return order == other.order && algo == other.algo;
+}
+bool ff::PolyhedralDiagnosis::operator!=(const PolyhedralDiagnosis& other) const
+{
+    return !(*this == other);
+}
+#endif
+
+//  ************************************************************************************************
+//  PolyhedralEdge implementation
+//  ************************************************************************************************
+
+ff::PolyhedralEdge::PolyhedralEdge(R3 Vlow, R3 Vhig) : m_E((Vhig - Vlow) / 2), m_R((Vhig + Vlow) / 2)
+{
+    if (m_E.mag2() == 0)
+        throw std::invalid_argument("At least one edge has zero length");
+}
+
+//! Returns sum_l=0^M/2 u^2l v^(M-2l) / (2l+1)!(M-2l)! - vperp^M/M!
+
+complex_t ff::PolyhedralEdge::contrib(int M, C3 qpa, complex_t qrperp) const
+{
+    complex_t u = qE(qpa);
+    complex_t v2 = m_R.dot(qpa);
+    complex_t v1 = qrperp;
+    complex_t v = v2 + v1;
+    // std::cout << std::scientific << std::showpos << std::setprecision(16) << "contrib: u=" << u
+    //              << " v1=" << v1 << " v2=" << v2 << "\n";
+    if (v == 0.) { // only 2l=M contributes
+        if (M & 1) // M is odd
+            return 0.;
+        return ReciprocalFactorialArray[M] * (pow(u, M) / (M + 1.) - pow(v1, M));
+    }
+    complex_t ret = 0;
+    // the l=0 term, minus (qperp.R)^M, which cancels under the sum over E*contrib()
+    if (v1 == 0.) {
+        ret = ReciprocalFactorialArray[M] * pow(v2, M);
+    } else if (v2 == 0.) {
+        ; // leave ret=0
+    } else {
+        // binomial expansion
+        for (int mm = 1; mm <= M; ++mm) {
+            complex_t term = ReciprocalFactorialArray[mm] * ReciprocalFactorialArray[M - mm]
+                             * pow(v2, mm) * pow(v1, M - mm);
+            ret += term;
+            // std::cout << "contrib mm=" << mm << " t=" << term << " s=" << ret << "\n";
+        }
+    }
+    if (u == 0.)
+        return ret;
+    for (int l = 1; l <= M / 2; ++l) {
+        complex_t term = ReciprocalFactorialArray[M - 2 * l] * ReciprocalFactorialArray[2 * l + 1]
+                         * pow(u, 2 * l) * pow(v, M - 2 * l);
+        ret += term;
+        // std::cout << "contrib l=" << l << " t=" << term << " s=" << ret << "\n";
+    }
+    return ret;
+}
+
+//  ************************************************************************************************
+//  PolyhedralFace implementation
+//  ************************************************************************************************
+
+double ff::PolyhedralFace::qpa_limit_series = 1e-2;
+int ff::PolyhedralFace::n_limit_series = 20;
+
+//! Static method, returns diameter of circle that contains all vertices.
+
+double ff::PolyhedralFace::diameter(const std::vector<R3>& V)
+{
+    double diameterFace = 0;
+    for (size_t j = 0; j < V.size(); ++j)
+        for (size_t jj = j + 1; jj < V.size(); ++jj)
+            diameterFace = std::max(diameterFace, (V[j] - V[jj]).mag());
+    return diameterFace;
+}
+
+//! Sets internal variables for given vertex chain.
+
+//! @param V oriented vertex list
+//! @param _sym_S2 true if face has a perpedicular two-fold symmetry axis
+
+ff::PolyhedralFace::PolyhedralFace(const std::vector<R3>& V, bool _sym_S2) : sym_S2(_sym_S2)
+{
+    size_t NV = V.size();
+    if (!NV)
+        throw std::runtime_error("Invalid polyhedral face: no edges given");
+    if (NV < 3)
+        throw std::runtime_error("Invalid polyhedral face: less than three edges");
+
+    // compute radius in 2d and 3d
+    m_radius_2d = diameter(V) / 2;
+    m_radius_3d = 0;
+    for (const R3& v : V)
+        m_radius_3d = std::max(m_radius_3d, v.mag());
+
+    // Initialize list of 'edges'.
+    // Do not create an edge if two vertices are too close to each other.
+    // TODO This is implemented in a somewhat sloppy way: we just skip an edge if it would
+    //      be too short. This leaves tiny open edges. In a clean implementation, we
+    //      rather should merge adjacent vertices before generating edges.
+    for (size_t j = 0; j < NV; ++j) {
+        size_t jj = (j + 1) % NV;
+        if ((V[j] - V[jj]).mag() < 1e-14 * m_radius_2d)
+            continue; // distance too short -> skip this edge
+        edges.emplace_back(V[j], V[jj]);
+    }
+    size_t NE = edges.size();
+    if (NE < 3)
+        throw std::invalid_argument("Face has less than three non-vanishing edges");
+
+    // compute n_k, rperp
+    m_normal = R3();
+    for (size_t j = 0; j < NE; ++j) {
+        size_t jj = (j + 1) % NE;
+        R3 ee = edges[j].E().cross(edges[jj].E());
+        if (ee.mag2() == 0) {
+            throw std::runtime_error("Invalid polyhedral face: two adjacent edges are parallel");
+        }
+        m_normal += ee.unit();
+    }
+    m_normal /= NE;
+    m_rperp = 0;
+    for (size_t j = 0; j < NV; ++j)
+        m_rperp += V[j].dot(m_normal);
+    m_rperp /= NV;
+    // assert that the vertices lay in a plane
+    for (size_t j = 1; j < NV; ++j)
+        if (std::abs(V[j].dot(m_normal) - m_rperp) > 1e-14 * m_radius_3d)
+            throw std::runtime_error("Invalid polyhedral face: not planar");
+    // compute m_area
+    m_area = 0;
+    for (size_t j = 0; j < NV; ++j) {
+        size_t jj = (j + 1) % NV;
+        m_area += m_normal.dot(V[j].cross(V[jj])) / 2;
+    }
+    // only now deal with inversion symmetry
+    if (sym_S2) {
+        if (NE & 1)
+            throw std::runtime_error("Invalid polyhedral face: odd #edges violates symmetry S2");
+        NE /= 2;
+        for (size_t j = 0; j < NE; ++j) {
+            if (((edges[j].R() - m_rperp * m_normal) + (edges[j + NE].R() - m_rperp * m_normal))
+                    .mag()
+                > 1e-12 * m_radius_2d)
+                throw std::runtime_error(
+                    "Invalid polyhedral face: edge centers violate symmetry S2");
+            if ((edges[j].E() + edges[j + NE].E()).mag() > 1e-12 * m_radius_2d)
+                throw std::runtime_error(
+                    "Invalid polyhedral face: edge vectors violate symmetry S2");
+        }
+        // keep only half of the egdes
+        edges.erase(edges.begin() + NE, edges.end());
+    }
+}
+
+//! Sets qperp and qpa according to argument q and to this polygon's normal.
+
+void ff::PolyhedralFace::decompose_q(C3 q, complex_t& qperp, C3& qpa) const
+{
+    qperp = m_normal.dot(q);
+    qpa = q - qperp * m_normal;
+    // improve numeric accuracy:
+    qpa -= m_normal.dot(qpa) * m_normal;
+    if (qpa.mag() < eps * std::abs(qperp))
+        qpa = C3(0., 0., 0.);
+}
+
+//! Returns core contribution to f_n
+
+complex_t ff::PolyhedralFace::ff_n_core(int m, C3 qpa, complex_t qperp) const
+{
+    const C3 prevec = 2. * m_normal.cross(qpa); // complex conjugation not here but in .dot
+    complex_t ret = 0;
+    const complex_t qrperp = qperp * m_rperp;
+    for (size_t i = 0; i < edges.size(); ++i) {
+        const PolyhedralEdge& e = edges[i];
+        const complex_t vfac = prevec.dot(e.E());
+        const complex_t tmp = e.contrib(m + 1, qpa, qrperp);
+        ret += vfac * tmp;
+        //     std::cout << std::scientific << std::showpos << std::setprecision(16)
+        //               << "DBX ff_n_core " << m << " " << vfac << " " << tmp
+        //               << " term=" << vfac * tmp << " sum=" << ret << "\n";
+    }
+    return ret;
+}
+
+//! Returns contribution qn*f_n [of order q^(n+1)] from this face to the polyhedral form factor.
+
+complex_t ff::PolyhedralFace::ff_n(int n, C3 q) const
+{
+    complex_t qn = q.dot(m_normal); // conj(q)*normal (dot is antilinear in 'this' argument)
+    if (std::abs(qn) < eps * q.mag())
+        return 0.;
+    complex_t qperp;
+    C3 qpa;
+    decompose_q(q, qperp, qpa);
+    double qpa_mag2 = qpa.mag2();
+    if (qpa_mag2 == 0.)
+        return qn * pow(qperp * m_rperp, n) * m_area * ReciprocalFactorialArray[n];
+    if (sym_S2)
+        return qn * (ff_n_core(n, qpa, qperp) + ff_n_core(n, -qpa, qperp)) / qpa_mag2;
+    complex_t tmp = ff_n_core(n, qpa, qperp);
+    // std::cout << "DBX ff_n " << n << " " << qn << " " << tmp << " " << qpa_mag2 << "\n";
+    return qn * tmp / qpa_mag2;
+}
+
+//! Returns sum of n>=1 terms of qpa expansion of 2d form factor
+
+complex_t ff::PolyhedralFace::expansion(complex_t fac_even, complex_t fac_odd, C3 qpa,
+                                    double abslevel) const
+{
+#ifdef ALGORITHM_DIAGNOSTIC
+    polyhedralDiagnosis.algo += 1;
+#endif
+    complex_t sum = 0;
+    complex_t n_fac = I;
+    int count_return_condition = 0;
+    for (int n = 1; n < n_limit_series; ++n) {
+#ifdef ALGORITHM_DIAGNOSTIC
+        polyhedralDiagnosis.order = std::max(polyhedralDiagnosis.order, n);
+#endif
+        complex_t term = n_fac * (n & 1 ? fac_odd : fac_even) * ff_n_core(n, qpa, 0) / qpa.mag2();
+        // std::cout << std::setprecision(16) << "    sum=" << sum << " +term=" << term << "\n";
+        sum += term;
+        if (std::abs(term) <= eps * std::abs(sum) || std::abs(sum) < eps * abslevel)
+            ++count_return_condition;
+        else
+            count_return_condition = 0;
+        if (count_return_condition > 2)
+            return sum; // regular exit
+        n_fac = mul_I(n_fac);
+    }
+    throw std::runtime_error("Numeric error in polyhedral face: series f(q_pa) not converged");
+}
+
+//! Returns core contribution to analytic 2d form factor.
+
+complex_t ff::PolyhedralFace::edge_sum_ff(C3 q, C3 qpa, bool sym_Ci) const
+{
+    C3 prevec = m_normal.cross(qpa); // complex conjugation will take place in .dot
+    complex_t sum = 0;
+    complex_t vfacsum = 0;
+    for (size_t i = 0; i < edges.size(); ++i) {
+        const ff::PolyhedralEdge& e = edges[i];
+        complex_t qE = e.qE(qpa);
+        complex_t qR = e.qR(qpa);
+        complex_t Rfac = sym_S2 ? sin(qR) : (sym_Ci ? cos(e.qR(q)) : exp_I(qR));
+        complex_t vfac;
+        if (sym_S2 || i < edges.size() - 1) {
+            vfac = prevec.dot(e.E());
+            vfacsum += vfac;
+        } else {
+            vfac = -vfacsum; // to improve numeric accuracy: qcE_J = - sum_{j=0}^{J-1} qcE_j
+        }
+        complex_t term = vfac * sinc(qE) * Rfac;
+        sum += term;
+        //    std::cout << std::scientific << std::showpos << std::setprecision(16)
+        //              << "    sum=" << sum << " term=" << term << " vf=" << vfac << " qE=" << qE
+        //              << " qR=" << qR << " sinc=" << sinc(qE) << " Rfac=" << Rfac << "\n";
+    }
+    return sum;
+}
+
+//! Returns the contribution ff(q) of this face to the polyhedral form factor.
+
+complex_t ff::PolyhedralFace::ff(C3 q, bool sym_Ci) const
+{
+    complex_t qperp;
+    C3 qpa;
+    decompose_q(q, qperp, qpa);
+    double qpa_red = m_radius_2d * qpa.mag();
+    complex_t qr_perp = qperp * m_rperp;
+    complex_t ff0 = (sym_Ci ? 2. * I * sin(qr_perp) : exp_I(qr_perp)) * m_area;
+    if (qpa_red == 0)
+        return ff0;
+    if (qpa_red < qpa_limit_series && !sym_S2) {
+        // summation of power series
+        complex_t fac_even;
+        complex_t fac_odd;
+        if (sym_Ci) {
+            fac_even = 2. * mul_I(sin(qr_perp));
+            fac_odd = 2. * cos(qr_perp);
+        } else {
+            fac_even = exp_I(qr_perp);
+            fac_odd = fac_even;
+        }
+        return ff0 + expansion(fac_even, fac_odd, qpa, std::abs(ff0));
+    }
+    // direct evaluation of analytic formula
+    complex_t prefac;
+    if (sym_S2)
+        prefac = sym_Ci ? -8. * sin(qr_perp) : 4. * mul_I(exp_I(qr_perp));
+    else
+        prefac = sym_Ci ? 4. : 2. * exp_I(qr_perp);
+    // std::cout << "       qrperp=" << qr_perp << " => prefac=" << prefac << "\n";
+    return prefac * edge_sum_ff(q, qpa, sym_Ci) / mul_I(qpa.mag2());
+}
+
+//! Two-dimensional form factor, for use in prism, from power series.
+
+complex_t ff::PolyhedralFace::ff_2D_expanded(C3 qpa) const
+{
+    return m_area + expansion(1., 1., qpa, std::abs(m_area));
+}
+
+//! Two-dimensional form factor, for use in prism, from sum over edge form factors.
+
+complex_t ff::PolyhedralFace::ff_2D_direct(C3 qpa) const
+{
+    return (sym_S2 ? 4. : 2. / I) * edge_sum_ff(qpa, qpa, false) / qpa.mag2();
+}
+
+//! Returns the two-dimensional form factor of this face, for use in a prism.
+
+complex_t ff::PolyhedralFace::ff_2D(C3 qpa) const
+{
+    if (std::abs(qpa.dot(m_normal)) > eps * qpa.mag())
+        throw std::runtime_error(
+            "Numeric error in polyhedral formfactor: ff_2D called with perpendicular q component");
+    double qpa_red = m_radius_2d * qpa.mag();
+    if (qpa_red == 0)
+        return m_area;
+    if (qpa_red < qpa_limit_series && !sym_S2)
+        return ff_2D_expanded(qpa);
+    return ff_2D_direct(qpa);
+}
+
+//! Throws if deviation from inversion symmetry is detected. Does not check vertices.
+
+void ff::PolyhedralFace::assert_Ci(const PolyhedralFace& other) const
+{
+    if (std::abs(m_rperp - other.m_rperp) > 1e-15 * (m_rperp + other.m_rperp))
+        throw std::runtime_error(
+            "Invalid polyhedron: faces with different distance from origin violate symmetry Ci");
+    if (std::abs(m_area - other.m_area) > 1e-15 * (m_area + other.m_area))
+        throw std::runtime_error(
+            "Invalid polyhedron: faces with different areas violate symmetry Ci");
+    if ((m_normal + other.m_normal).mag() > 1e-14)
+        throw std::runtime_error(
+            "Invalid polyhedron: faces do not have opposite orientation, violating symmetry Ci");
+}
diff --git a/ff/PolyhedralComponents.h b/ff/PolyhedralComponents.h
new file mode 100644
index 00000000000..41405221ce0
--- /dev/null
+++ b/ff/PolyhedralComponents.h
@@ -0,0 +1,103 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/PolyhedralComponents.h
+//! @brief     Defines classes PolyhedralEdge, PolyhedralFace
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+#ifdef SWIG
+#error no need to expose this header to Swig
+#endif
+
+#ifndef USER_API
+#ifndef LIBFORMFACTOR_LIB_POLYHEDRALCOMPONENTS_H
+#define LIBFORMFACTOR_LIB_POLYHEDRALCOMPONENTS_H
+
+#include <heinz/Complex.h>
+#include <heinz/Vectors3D.h>
+#include <vector>
+
+namespace ff {
+
+#ifdef ALGORITHM_DIAGNOSTIC
+#include <string>
+
+struct PolyhedralDiagnosis {
+    int algo;
+    int order;
+    std::string msg;
+    void reset();
+    std::string message() const;
+    bool operator==(const PolyhedralDiagnosis&) const;
+    bool operator!=(const PolyhedralDiagnosis&) const;
+};
+inline PolyhedralDiagnosis polyhedralDiagnosis;
+#endif
+
+//! One edge of a polygon, for form factor computation.
+
+class PolyhedralEdge {
+public:
+    PolyhedralEdge(R3 Vlow, R3 Vhig);
+
+    R3 E() const { return m_E; }
+    R3 R() const { return m_R; }
+    complex_t qE(C3 q) const { return m_E.dot(q); }
+    complex_t qR(C3 q) const { return m_R.dot(q); }
+
+    complex_t contrib(int m, C3 qpa, complex_t qrperp) const;
+
+private:
+    R3 m_E; //!< vector pointing from mid of edge to upper vertex
+    R3 m_R; //!< position vector of edge midpoint
+};
+
+//! A polygon, for form factor computation.
+
+class PolyhedralFace {
+public:
+    static double diameter(const std::vector<R3>& V);
+
+    PolyhedralFace(const std::vector<R3>& _V = std::vector<R3>(), bool _sym_S2 = false);
+
+    double area() const { return m_area; }
+    double pyramidalVolume() const { return m_rperp * m_area / 3; }
+    double radius3d() const { return m_radius_3d; }
+    //! Returns conj(q)*normal [BasicVector3D::dot is antilinear in 'this' argument]
+    complex_t normalProjectionConj(C3 q) const { return q.dot(m_normal); }
+    complex_t ff_n(int n, C3 q) const;
+    complex_t ff(C3 q, bool sym_Ci) const;
+    complex_t ff_2D(C3 qpa) const;
+    complex_t ff_2D_direct(C3 qpa) const;   // for TestTriangle
+    complex_t ff_2D_expanded(C3 qpa) const; // for TestTriangle
+    void assert_Ci(const PolyhedralFace& other) const;
+
+private:
+    static double qpa_limit_series; //!< determines when use power series
+    static int n_limit_series;
+
+    bool sym_S2; //!< if true, then edges obtainable by inversion are not provided
+    std::vector<PolyhedralEdge> edges;
+    double m_area;
+    R3 m_normal;        //!< normal vector of this polygon's plane
+    double m_rperp;     //!< distance of this polygon's plane from the origin, along 'm_normal'
+    double m_radius_2d; //!< radius of enclosing cylinder
+    double m_radius_3d; //!< radius of enclosing sphere
+
+    void decompose_q(C3 q, complex_t& qperp, C3& qpa) const;
+    complex_t ff_n_core(int m, C3 qpa, complex_t qperp) const;
+    complex_t edge_sum_ff(C3 q, C3 qpa, bool sym_Ci) const;
+    complex_t expansion(complex_t fac_even, complex_t fac_odd, C3 qpa, double abslevel) const;
+};
+
+} // namespace ff
+
+#endif // LIBFORMFACTOR_LIB_POLYHEDRALCOMPONENTS_H
+#endif // USER_API
diff --git a/ff/PolyhedralTopology.h b/ff/PolyhedralTopology.h
new file mode 100644
index 00000000000..b3e4b99ba2d
--- /dev/null
+++ b/ff/PolyhedralTopology.h
@@ -0,0 +1,44 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/PolyhedralTopology.h
+//! @brief     Defines classes PolygonalTopology, PolyhedralTopology
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+#ifdef SWIG
+#error no need to expose this header to Swig
+#endif
+
+#ifndef USER_API
+#ifndef LIBFORMFACTOR_LIB_POLYHEDRALTOPOLOGY_H
+#define LIBFORMFACTOR_LIB_POLYHEDRALTOPOLOGY_H
+
+#include <vector>
+
+namespace ff {
+
+//! For internal use in PolyhedralFace.
+class PolygonalTopology {
+public:
+    std::vector<int> vertexIndices;
+    bool symmetry_S2;
+};
+
+//! For internal use in IFormFactorPolyhedron.
+class PolyhedralTopology {
+public:
+    std::vector<PolygonalTopology> faces;
+    bool symmetry_Ci;
+};
+
+} // namespace ff
+
+#endif // LIBFORMFACTOR_LIB_POLYHEDRALTOPOLOGY_H
+#endif // USER_API
diff --git a/ff/Polyhedron.cpp b/ff/Polyhedron.cpp
new file mode 100644
index 00000000000..87584be8bc5
--- /dev/null
+++ b/ff/Polyhedron.cpp
@@ -0,0 +1,192 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/Polyhedron.cpp
+//! @brief     Implements class Polyhedron.
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+//! The mathematics implemented here is described in full detail in a paper
+//! by Joachim Wuttke, entitled
+//! "Numerically stable form factor of any polygon and polyhedron"
+
+#include "ff/Polyhedron.h"
+#include "ff/PolyhedralComponents.h"
+#include "ff/PolyhedralTopology.h"
+#include <stdexcept>
+
+#ifdef ALGORITHM_DIAGNOSTIC_LEVEL2
+#include <boost/format.hpp>
+#endif
+
+namespace {
+
+const double eps = 2e-16;
+const double q_limit_series = 1e-2;
+const int n_limit_series = 20;
+
+} // namespace
+
+
+ff::Polyhedron::Polyhedron(const PolyhedralTopology& topology, double z_bottom,
+                           const std::vector<R3>& vertices)
+{
+    m_vertices.clear();
+    for (const R3& vertex : vertices)
+        m_vertices.push_back(vertex - R3{0, 0, z_bottom});
+
+    m_z_bottom = z_bottom;
+    m_sym_Ci = topology.symmetry_Ci;
+
+    double diameter = 0;
+    for (size_t j = 0; j < vertices.size(); ++j)
+        for (size_t jj = j + 1; jj < vertices.size(); ++jj)
+            diameter = std::max(diameter, (vertices[j] - vertices[jj]).mag());
+
+    m_faces.clear();
+    for (const PolygonalTopology& tf : topology.faces) {
+        std::vector<R3> corners; // of one face
+        for (int i : tf.vertexIndices)
+            corners.push_back(vertices[i]);
+        if (PolyhedralFace::diameter(corners) <= 1e-14 * diameter)
+            continue; // skip ridiculously small face
+        m_faces.emplace_back(corners, tf.symmetry_S2);
+    }
+    if (m_faces.size() < 4)
+        throw std::runtime_error("Invalid polyhedron: less than four non-vanishing faces");
+
+    m_radius = 0;
+    m_volume = 0;
+    for (const PolyhedralFace& Gk : m_faces) {
+        m_radius = std::max(m_radius, Gk.radius3d());
+        m_volume += Gk.pyramidalVolume();
+    }
+    if (m_sym_Ci) {
+        if (m_faces.size() & 1)
+            throw std::runtime_error("Invalid polyhedron: odd #faces violates symmetry Ci");
+        size_t N = m_faces.size() / 2;
+        // for this tests, m_faces must be in a specific order
+        for (size_t k = 0; k < N; ++k)
+            m_faces[k].assert_Ci(m_faces[2 * N - 1 - k]);
+        // keep only half of the faces
+        m_faces.erase(m_faces.begin() + N, m_faces.end());
+    }
+}
+
+void ff::Polyhedron::assert_platonic() const
+{
+    // just one test; one could do much more ...
+    double pyramidal_volume = 0;
+    for (const auto& Gk : m_faces)
+        pyramidal_volume += Gk.pyramidalVolume();
+    pyramidal_volume /= m_faces.size();
+    for (const auto& Gk : m_faces)
+        if (std::abs(Gk.pyramidalVolume() - pyramidal_volume) > 160 * eps * pyramidal_volume)
+            throw std::runtime_error(
+                "Invalid Polyhedron: declared platonic but not sufficiently uniform");
+}
+
+double ff::Polyhedron::volume() const
+{
+    return m_volume;
+}
+
+double ff::Polyhedron::radius() const
+{
+    return m_radius;
+}
+
+const std::vector<R3> ff::Polyhedron::vertices() const
+{
+    std::vector<R3> ret;
+    ret.reserve(m_vertices.size());
+    for (const auto& vertex : m_vertices)
+        ret.emplace_back(R3{vertex});
+    return ret;
+}
+
+//! Returns the form factor F(q) of this polyhedron, respecting the offset z_bottom.
+
+complex_t ff::Polyhedron::evaluate_for_q(const C3& _q) const
+{
+    C3 q{_q};
+    return exp_I(-m_z_bottom * q.z()) * evaluate_centered(q);
+}
+
+//! Returns the form factor F(q) of this polyhedron, with origin at z=0.
+
+complex_t ff::Polyhedron::evaluate_centered(const C3& _q) const
+{
+    C3 q{_q};
+    double q_red = m_radius * q.mag();
+#ifdef ALGORITHM_DIAGNOSTIC
+    polyhedralDiagnosis.reset();
+#endif
+    if (q_red == 0)
+        return m_volume;
+    if (q_red < q_limit_series) {
+        // summation of power series
+#ifdef ALGORITHM_DIAGNOSTIC
+        polyhedralDiagnosis.algo = 100;
+#endif
+        complex_t sum = 0;
+        complex_t n_fac = (m_sym_Ci ? -2 : -1) / q.mag2();
+        int count_return_condition = 0;
+        for (int n = 2; n < n_limit_series; ++n) {
+            if (m_sym_Ci && n & 1)
+                continue;
+#ifdef ALGORITHM_DIAGNOSTIC
+            polyhedralDiagnosis.order = std::max(polyhedralDiagnosis.order, n);
+#endif
+            complex_t term = 0;
+            for (const PolyhedralFace& Gk : m_faces) {
+                complex_t tmp = Gk.ff_n(n + 1, q);
+                term += tmp;
+            }
+            term *= n_fac;
+#ifdef ALGORITHM_DIAGNOSTIC_LEVEL2
+            polyhedralDiagnosis.msg +=
+                boost::str(boost::format("  + term(n=%2i) = %23.17e+i*%23.17e\n") % n % term.real()
+                           % term.imag());
+#endif
+            sum += term;
+            if (std::abs(term) <= eps * std::abs(sum) || std::abs(sum) < eps * m_volume)
+                ++count_return_condition;
+            else
+                count_return_condition = 0;
+            if (count_return_condition > 2)
+                return m_volume + sum; // regular exit
+            n_fac = m_sym_Ci ? -n_fac : mul_I(n_fac);
+        }
+        throw std::runtime_error("Numeric failure in polyhedron: series F(q) not converged");
+    }
+
+    // direct evaluation of analytic formula (coefficients may involve series)
+#ifdef ALGORITHM_DIAGNOSTIC
+    polyhedralDiagnosis.algo = 200;
+#endif
+    complex_t sum = 0;
+    for (const PolyhedralFace& Gk : m_faces) {
+        complex_t qn = Gk.normalProjectionConj(q); // conj(q)*normal
+        if (std::abs(qn) < eps * q.mag())
+            continue;
+        complex_t term = qn * Gk.ff(q, m_sym_Ci);
+#ifdef ALGORITHM_DIAGNOSTIC //_LEVEL2
+        polyhedralDiagnosis.msg += boost::str(boost::format("  + face_ff = %23.17e+i*%23.17e\n")
+                                              % term.real() % term.imag());
+#endif
+        sum += term;
+    }
+#ifdef ALGORITHM_DIAGNOSTIC //_LEVEL2
+    polyhedralDiagnosis.msg +=
+        boost::str(boost::format(" -> raw sum = %23.17e+i*%23.17e; divisor = %23.17e\n")
+                   % sum.real() % sum.imag() % q.mag2());
+#endif
+    return sum / I / q.mag2();
+}
diff --git a/ff/Polyhedron.h b/ff/Polyhedron.h
new file mode 100644
index 00000000000..c8cc56a2438
--- /dev/null
+++ b/ff/Polyhedron.h
@@ -0,0 +1,64 @@
+//  ************************************************************************************************
+//
+//  libformfactor: efficient and accurate computation of scattering form factors
+//
+//! @file      lib/Polyhedron.h
+//! @brief     Defines class Polyhedron.
+//!
+//! @homepage  http://www.bornagainproject.org
+//! @license   GNU General Public License v3 or higher (see LICENSE)
+//! @copyright Forschungszentrum Jülich GmbH 2021
+//! @author    Joachim Wuttke, Scientific Computing Group at MLZ (see CITATION)
+//
+//  ************************************************************************************************
+
+#ifdef SWIG
+#error no need to expose this header to Swig
+#endif
+
+#ifndef USER_API
+#ifndef LIBFORMFACTOR_LIB_POLYHEDRON_H
+#define LIBFORMFACTOR_LIB_POLYHEDRON_H
+
+#include <array>
+#include <memory>
+#include <vector>
+
+#include <heinz/Complex.h>
+#include <heinz/Vectors3D.h>
+
+namespace ff {
+
+class PolyhedralFace;
+class PolyhedralTopology;
+
+//! A polyhedron, implementation class for use in IFormFactorPolyhedron
+
+class Polyhedron {
+public:
+    Polyhedron(const PolyhedralTopology& topology, double z_bottom,
+               const std::vector<R3>& vertices);
+    Polyhedron(const Polyhedron&) = delete;
+
+    void assert_platonic() const;
+    double volume() const;
+    double radius() const;
+
+    const std::vector<R3> vertices() const; //! needed for topZ, bottomZ computation
+    complex_t evaluate_for_q(const C3& q) const;
+    complex_t evaluate_centered(const C3& q) const;
+
+private:
+    double m_z_bottom;
+    bool m_sym_Ci; //!< if true, then faces obtainable by inversion are not provided
+
+    std::vector<PolyhedralFace> m_faces;
+    double m_radius;
+    double m_volume;
+    std::vector<R3> m_vertices; //! for topZ, bottomZ computation only
+};
+
+} // namespace ff
+
+#endif // LIBFORMFACTOR_LIB_POLYHEDRON_H
+#endif // USER_API
-- 
GitLab