//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sample/Particle/Mesocrystal.cpp
//! @brief     Implements class Mesocrystal.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Sample/Particle/Mesocrystal.h"
#include "Base/Math/Numeric.h"
#include "Base/Spin/SpinMatrix.h"
#include "Base/Util/Assert.h"
#include "Base/Vector/WavevectorInfo.h"
#include "Sample/Element/DiffuseElement.h"
#include "Sample/Lattice/Lattice3D.h"
#include "Sample/Lattice/SelectionRule.h"
#include "Sample/Material/Material.h"
#include "Sample/Option/SimulationOptions.h"
#include "Sample/Particle/Compound.h"
#include "Sample/Particle/CoreAndShell.h"
#include "Sample/Particle/Crystal.h"
#include "Sample/Particle/DwbaChannels.h"
#include "Sample/Particle/HardFormfactor.h"
#include "Sample/Particle/Mixture.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Particle/Rotations.h"
#include <algorithm>
#include <numbers>
#include <stdexcept>

using std::numbers::pi;

namespace {

//! Recursively prepares a mesocrystal basis in lab coordinates.
//! Parent rotation and translation are pushed down to children.
std::unique_ptr<IParticle> processBasis(const IParticle* basis, const Material& ambientMat,
                                        const MesoOptions& meso_options)
{
    const auto* rot = basis->particleRotation();
    const R3& translation = basis->particlePosition();

    if (const auto* b = dynamic_cast<const Compound*>(basis)) {
        auto result = std::make_unique<Compound>();
        for (const auto* particle : b->components()) {
            std::unique_ptr<IParticle> transformed_child(particle->clone());
            transformed_child->rotate(rot);
            transformed_child->translate(translation);

            auto processed = processBasis(transformed_child.get(), ambientMat, meso_options);
            result->addComponent(std::move(processed));
        }
        return result;
    }
    if (const auto* b = dynamic_cast<const Mixture*>(basis)) {
        auto result = std::make_unique<Mixture>();
        const auto& abundances = b->abundances();
        const auto& particles = b->particles();
        for (size_t i = 0; i < particles.size(); ++i) {
            std::unique_ptr<IParticle> transformed_child(particles[i]->clone());
            transformed_child->rotate(rot);
            transformed_child->translate(translation);

            auto processed = processBasis(transformed_child.get(), ambientMat, meso_options);
            result->addParticle(std::move(processed), abundances[i]);
        }
        result->validate();
        return result;
    }
    if (const auto* b = dynamic_cast<const CoreAndShell*>(basis)) {
        std::unique_ptr<IParticle> transformed_shell(b->shellParticle()->clone());
        transformed_shell->rotate(rot);
        transformed_shell->translate(translation);
        auto processed_shell = processBasis(transformed_shell.get(), ambientMat, meso_options);
        auto* processed_shell_particle = dynamic_cast<Particle*>(processed_shell.get());
        ASSERT(processed_shell_particle);

        std::unique_ptr<IParticle> transformed_core(b->coreParticle()->clone());
        transformed_core->rotate(rot);
        transformed_core->translate(translation);
        auto processed_core = processBasis(transformed_core.get(),
                                           *processed_shell_particle->material(), meso_options);

        return std::make_unique<CoreAndShell>(*processed_core, *processed_shell_particle);
    }
    if (const auto* b = dynamic_cast<const Mesocrystal*>(basis)) {
        auto result = std::unique_ptr<Mesocrystal>(b->clone());
        result->prepareForComputation(ambientMat, meso_options);
        return result;
    }

    // Remaining case: the basis is a single particle.
    const auto* p = dynamic_cast<const Particle*>(basis);
    ASSERT(p);

    auto result = std::unique_ptr<Particle>(p->clone());

    std::unique_ptr<Material> transformed_material(
        rot ? p->material()->rotatedMaterial(rot->rotMatrix())
            : std::unique_ptr<Material>(p->material()->clone()));
    result->setMaterial(*transformed_material);
    result->setAmbientMaterial(ambientMat);
    return result;
}

bool containsMixtures(const IParticle& particle)
{
    if (dynamic_cast<const Mixture*>(&particle))
        return true;

    if (const auto* core_shell = dynamic_cast<const CoreAndShell*>(&particle))
        return containsMixtures(*core_shell->coreParticle())
               || containsMixtures(*core_shell->shellParticle());

    if (const auto* meso = dynamic_cast<const Mesocrystal*>(&particle))
        return containsMixtures(*meso->particleStructure().basis());

    if (const auto* compound = dynamic_cast<const Compound*>(&particle)) {
        for (const auto* child : compound->components())
            if (containsMixtures(*child))
                return true;
        return false;
    }

    return false;
}

} // namespace


Mesocrystal::Mesocrystal(const Crystal& crystal, const HardFormfactor& formfactor)
    : m_crystal(crystal.clone())
    , m_outer_formfactor(formfactor.clone())
{
}

Mesocrystal::~Mesocrystal() = default;

Mesocrystal* Mesocrystal::clone() const
{
    auto* result = new Mesocrystal(*m_crystal, *m_outer_formfactor);
    instructClone(result);
    if (m_admix_volume)
        result->setVolumeToAdmix(*m_admix_volume);
    return result;
}

void Mesocrystal::setSliceNumber(size_t n)
{
    IParticle::setSliceNumber(n);
    if (m_lab_basis)
        m_lab_basis->setSliceNumber(n);
}

const Crystal& Mesocrystal::particleStructure() const
{
    ASSERT(m_crystal);
    return *m_crystal;
}

double Mesocrystal::bottomZ() const
{
    return m_outer_formfactor->zBottom(particleRotation()) + particlePosition().z();
}

double Mesocrystal::topZ() const
{
    return m_outer_formfactor->zTop(particleRotation()) + particlePosition().z();
}

double Mesocrystal::volumeToAdmix() const
{
    if (m_admix_volume)
        return *m_admix_volume;

    const double basis_volume = m_crystal->basis()->volumeToAdmix();
    return basis_volume * numNodes();
}

void Mesocrystal::setVolumeToAdmix(double volume)
{
    m_admix_volume = volume;
}

double Mesocrystal::slicedVolumeToAdmix(const Particle& sliced_outer_shape) const
{
    // This slicing workaround to be executed in the full shape source mesocrystal.
    // It is needed only because a fine slicing of mesocrystal may lead to assignment
    // of bulky unsliced nodes to the thin slice and thus cause too high unfeasible spikes on the
    // SLD profile.
    const double basis_volume = m_crystal->basis()->volumeToAdmix();
    const ShapeIndexes indexes = calcBasisIndexes();
    const double full_volume = m_outer_formfactor->volume();
    const double slice_volume = sliced_outer_shape.pFormfactor()->volume();
    if (full_volume > 0.0 && slice_volume / full_volume > 0.999)
        return basis_volume * indexes.nodes.size();

    const auto lab_crystal = std::unique_ptr<Crystal>(
        particleStructure().transformed(particlePosition(), particleRotation()));
    const double slice_z_lab_low = sliced_outer_shape.bottomZ() - particlePosition().z();
    const double slice_z_lab_hig = sliced_outer_shape.topZ() - particlePosition().z();
    std::vector<std::unique_ptr<IParticle>> components = m_crystal->basis()->decompose();

    double total_volume = 0.0;
    for (const I3& index : indexes.nodes) {
        R3 lattice_lab = index.x() * lab_crystal->lattice()->basisVectorA()
                         + index.y() * lab_crystal->lattice()->basisVectorB()
                         + index.z() * lab_crystal->lattice()->basisVectorC();

        for (const auto& comp : components) {
            R3 comp_pos = comp->particlePosition();
            if (particleRotation())
                comp_pos = particleRotation()->transformed(comp_pos);

            const double comp_center_z = lattice_lab.z() + comp_pos.z();
            const double ff_z_low = comp->bottomZ() - comp->particlePosition().z();
            const double ff_z_hig = comp->topZ() - comp->particlePosition().z();
            const double comp_height = ff_z_hig - ff_z_low;
            if (comp_height <= 0)
                continue;

            const double particle_z_low = comp_center_z + ff_z_low;
            const double particle_z_hig = comp_center_z + ff_z_hig;
            const double overlap_low = std::max(particle_z_low, slice_z_lab_low);
            const double overlap_hig = std::min(particle_z_hig, slice_z_lab_hig);
            const double overlap = std::max(0.0, overlap_hig - overlap_low);
            total_volume += (overlap / comp_height) * comp->volumeToAdmix();
        }
    }

    return total_volume;
}

std::unique_ptr<const Material> Mesocrystal::materialToAdmix() const
{
    ASSERT(m_crystal);
    return m_crystal->basis()->materialToAdmix();
}

bool Mesocrystal::hasRadius() const
{
    return m_crystal->basis()->hasRadius();
}

double Mesocrystal::radius() const
{
    return m_outer_formfactor->radialExtension() + m_crystal->basis()->radius();
}

double Mesocrystal::radialExtension() const
{
    return m_outer_formfactor->radialExtension();
}

bool Mesocrystal::containsSoftParticle() const
{
    return m_crystal->basis()->containsSoftParticle();
}

std::vector<R3> Mesocrystal::calcBasisPositions() const
{
    const auto& nodes = basisIndexes().nodes;
    std::vector<R3> result(nodes.size());
    size_t counter = 0;
    for (const I3& node : nodes)
        // position = i*a + j*b + k*c in mesocrystal-local coordinates
        result[counter++] = node.x() * m_crystal->lattice()->basisVectorA()
                            + node.y() * m_crystal->lattice()->basisVectorB()
                            + node.z() * m_crystal->lattice()->basisVectorC();
    return result;
}

void Mesocrystal::prepareForComputation(const Material& ambientMat, const MesoOptions& meso_options)
{
    m_radius_factor = meso_options.radius_factor;
    m_use_reciprocal_sum = meso_options.use_reciprocal_sum;

    const std::unique_ptr<Crystal> lab_crystal(
        particleStructure().transformed(particlePosition(), particleRotation()));

    m_lab_lattice = std::make_unique<const Lattice3D>(*lab_crystal->lattice());
    m_lab_basis = processBasis(lab_crystal->basis(), ambientMat, meso_options);
    if (m_slice_number)
        m_lab_basis->setSliceNumber(*m_slice_number);

    if (m_use_reciprocal_sum) {
        if (containsMixtures(*m_lab_basis))
            throw std::runtime_error("Reciprocal-space moments require a deterministic basis "
                                     "(no Mixture in the basis tree)");
        calculateLargestReciprocalDistance();
    }

    m_cached_scattering_indexes = calcBasisIndexes();
}

complex_t Mesocrystal::theFF(const WavevectorInfo& wavevectors) const
{
    ASSERT(m_lab_basis && m_lab_lattice);
    if (m_use_reciprocal_sum)
        return reciprocalSpaceSum(wavevectors);
    return structureAmplitude(wavevectors.getQ()) * m_lab_basis->theFF(wavevectors);
}

SpinMatrix Mesocrystal::thePolFF(const WavevectorInfo& wavevectors) const
{
    ASSERT(m_lab_basis && m_lab_lattice);
    if (m_use_reciprocal_sum)
        return reciprocalSpaceSumPol(wavevectors);
    return structureAmplitude(wavevectors.getQ()) * m_lab_basis->thePolFF(wavevectors);
}

ScatteringMoments Mesocrystal::scatteringMoments(const DiffuseElement& ele) const
{
    const DwbaChannels dwba = extractDwbaChannels(ele, sliceNumber());
    const ChannelMoments moments = channelMoments(dwba);

    complex_t mean = 0;
    double var = 0;
    for (size_t m = 0; m < dwba.n_active; ++m) {
        mean += dwba.w[m] * moments.mu[m];
        for (size_t n = 0; n < dwba.n_active; ++n)
            var += std::real(dwba.w[m] * std::conj(dwba.w[n]) * moments.C[m][n]);
    }
    return {mean, std::max(0.0, var)};
}

PolScatteringMoments Mesocrystal::polScatteringMoments(const DiffuseElement& ele) const
{
    const PolDwbaChannels dwba = extractPolDwbaChannels(ele, sliceNumber());
    const PolChannelMoments moments = polChannelMoments(dwba);

    SpinMatrix mean;
    SpinMatrix var;
    for (size_t m = 0; m < dwba.n_active; ++m) {
        mean += applyWeight(moments.mu[m], dwba.flux[m]);
        for (size_t n = 0; n < dwba.n_active; ++n)
            var += moments.C[m][n];
    }
    return {mean, var};
}

ChannelMoments Mesocrystal::channelMoments(const DwbaChannels& channels) const
{
    ASSERT(m_lab_basis && m_lab_lattice);

    const ShapeIndexes& shape = basisIndexes();
    if (shape.nodes.empty())
        return ChannelMoments{};

    if (m_use_reciprocal_sum) {
        ChannelMoments result;
        for (size_t m = 0; m < channels.n_active; ++m)
            result.mu[m] = theFF(channels.q[m]);
        return result;
    }

    const auto basis = m_lab_basis->channelMoments(channels);
    ChannelMoments result;

    // The structure amplitude S(q) = sum_j exp(i q . R_j) * DW(q).
    // Mean: mu_M[m] = S(q_m) * mu_B[m].
    // The transfer factors T[m][n] use q_m - conj(q_n), with T[n][m] = conj(T[m][n]).
    for (size_t m = 0; m < channels.n_active; ++m) {
        const C3 q_m = channels.q[m].getQ();
        result.mu[m] = structureAmplitude(q_m) * basis.mu[m];
        result.C[m][m] = structureAmplitude(q_m - q_m.conj()) * basis.C[m][m];

        for (size_t n = m + 1; n < channels.n_active; ++n) {
            const C3 dq_mn = q_m - channels.q[n].getQ().conj();
            const complex_t t_mn = structureAmplitude(dq_mn);
            result.C[m][n] = t_mn * basis.C[m][n];
            result.C[n][m] = std::conj(t_mn) * basis.C[n][m];
        }
    }

    return result;
}

PolChannelMoments Mesocrystal::polChannelMoments(const PolDwbaChannels& channels) const
{
    ASSERT(m_lab_basis && m_lab_lattice);

    const ShapeIndexes& shape = basisIndexes();
    if (shape.nodes.empty())
        return PolChannelMoments{};

    if (m_use_reciprocal_sum) {
        PolChannelMoments result;
        for (size_t m = 0; m < channels.n_active; ++m)
            result.mu[m] = thePolFF(channels.q[m]);
        return result;
    }

    const auto basis = m_lab_basis->polChannelMoments(channels);
    PolChannelMoments result;

    // Same physics as scalar case, with SpinMatrix-valued basis moments.
    for (size_t m = 0; m < channels.n_active; ++m) {
        const C3 q_m = channels.q[m].getQ();
        result.mu[m] = structureAmplitude(q_m) * basis.mu[m];
        result.C[m][m] = structureAmplitude(q_m - q_m.conj()) * basis.C[m][m];

        for (size_t n = m + 1; n < channels.n_active; ++n) {
            const C3 dq_mn = q_m - channels.q[n].getQ().conj();
            const complex_t t_mn = structureAmplitude(dq_mn);
            result.C[m][n] = t_mn * basis.C[m][n];
            result.C[n][m] = std::conj(t_mn) * basis.C[n][m];
        }
    }

    return result;
}

R3 Mesocrystal::latticePosition(const I3& index) const
{
    const Lattice3D& lattice = *m_crystal->lattice();
    return index.x() * lattice.basisVectorA() + index.y() * lattice.basisVectorB()
           + index.z() * lattice.basisVectorC();
}

bool Mesocrystal::indexInShape(const I3& index) const
{
    R3 shifted_pos = latticePosition(index) + m_crystal->basis()->particlePosition();
    return m_outer_formfactor->contains(shifted_pos);
}

const Mesocrystal::ShapeIndexes& Mesocrystal::basisIndexes() const
{
    // Pre-computed in prepareForComputation(); fall back to on-the-fly.
    if (!m_cached_scattering_indexes)
        m_cached_scattering_indexes = calcBasisIndexes();
    return *m_cached_scattering_indexes;
}

Mesocrystal::ShapeIndexes Mesocrystal::calcBasisIndexes() const
{
    if (dynamic_cast<const SimpleSelectionRule*>(m_crystal->lattice()->selectionRule()))
        throw std::runtime_error("SimpleSelectionRule is incompatible with real-space lattice");

    struct Limits {
        int i_min = 0, i_max = 1;
        int j_min = 0, j_max = 1;
        int k_min = 0, k_max = 1;
    } limits;

    // First pass: grow a bounding box outward from the origin.
    // Conservative: may overshoot, but never misses an occupied node.
    auto expand_limits_if_occupied = [&](int i, int j, int k) {
        if (indexInShape(I3(i, j, k))) {
            if (i == limits.i_min)
                limits.i_min--;
            if (i == limits.i_max)
                limits.i_max++;

            if (j == limits.j_min)
                limits.j_min--;
            if (j == limits.j_max)
                limits.j_max++;

            if (k == limits.k_min)
                limits.k_min--;
            if (k == limits.k_max)
                limits.k_max++;
        }
    };

    auto iterate_i = [&](int j, int k) {
        for (int i = 1; i <= limits.i_max; i++)
            expand_limits_if_occupied(i, j, k);
        for (int i = 0; i >= limits.i_min; i--)
            expand_limits_if_occupied(i, j, k);
    };
    auto iterate_j = [&](int k) {
        for (int j = 1; j <= limits.j_max; j++)
            iterate_i(j, k);
        for (int j = 0; j >= limits.j_min; j--)
            iterate_i(j, k);
    };
    auto iterate_k = [&] {
        for (int k = 1; k <= limits.k_max; k++)
            iterate_j(k);
        for (int k = 0; k >= limits.k_min; k--)
            iterate_j(k);
    };
    iterate_k();

    // Coarse limits from the first pass; the second pass enumerates exactly.
    const I3 coarse_min_index(limits.i_min + 1, limits.j_min + 1, limits.k_min + 1);
    const I3 coarse_max_index(limits.i_max - 1, limits.j_max - 1, limits.k_max - 1);

    // Second pass: enumerate occupied nodes and build contiguous k-ranges
    // per (i,j) column, all in absolute lattice coordinates.
    std::vector<I3> basis_indexes;
    std::vector<ShapeIndexes::KColumn> k_columns;
    I3 min_index;
    I3 max_index;
    int max_k_count = 0;
    for (int i = coarse_min_index.x(); i <= coarse_max_index.x(); i++) {
        for (int j = coarse_min_index.y(); j <= coarse_max_index.y(); j++) {
            std::vector<ShapeIndexes::KRange> k_ranges;
            for (int k = coarse_min_index.z(); k <= coarse_max_index.z(); k++) {
                const I3 index(i, j, k);
                if (!indexInShape(index))
                    continue;

                basis_indexes.push_back(index);

                // Open or extend the current occupied run along k.
                if (k == coarse_min_index.z() || !indexInShape(I3(i, j, k - 1)))
                    k_ranges.push_back({k, k});

                if (k == coarse_max_index.z() || !indexInShape(I3(i, j, k + 1))) {
                    k_ranges.back().end = k;
                    max_k_count = std::max(max_k_count, k - k_ranges.back().begin + 1);
                }
            }

            if (!k_ranges.empty()) {
                // Cache per-axis bounds once while the occupied columns are enumerated.
                const I3 column_min(i, j, k_ranges.front().begin);
                const I3 column_max(i, j, k_ranges.back().end);
                if (k_columns.empty()) {
                    min_index = column_min;
                    max_index = column_max;
                } else {
                    min_index = I3(std::min(min_index.x(), column_min.x()),
                                   std::min(min_index.y(), column_min.y()),
                                   std::min(min_index.z(), column_min.z()));
                    max_index = I3(std::max(max_index.x(), column_max.x()),
                                   std::max(max_index.y(), column_max.y()),
                                   std::max(max_index.z(), column_max.z()));
                }

                k_columns.push_back({i, j, std::move(k_ranges)});
            }
        }
    }

    if (basis_indexes.empty())
        return {};

    return {std::move(basis_indexes), std::move(k_columns), min_index, max_index, max_k_count};
}

complex_t Mesocrystal::summedPhaseFactors(const C3& q, const Lattice3D& lattice,
                                          const ShapeIndexes& shape)
{
    // Early return for empty shapes (can happen for slices with no particles).
    if (shape.nodes.empty())
        return complex_t(0.0, 0.0);

    complex_t exp_a = exp_I(lattice.basisVectorA().dot(q));
    complex_t exp_b = exp_I(lattice.basisVectorB().dot(q));
    complex_t exp_c = exp_I(lattice.basisVectorC().dot(q));

    const auto& k_columns = shape.kColumns;
    ASSERT(!k_columns.empty());

    const int i0 = shape.min.x();
    const int j0 = shape.min.y();
    const int k0 = shape.min.z();

    // Precompute exp_a^i and exp_b^j via incremental multiplication.
    std::vector<complex_t> exponents_a(shape.max.x() - i0 + 1);
    std::vector<complex_t> exponents_b(shape.max.y() - j0 + 1);
    exponents_a.front() = pow(exp_a, i0);
    exponents_b.front() = pow(exp_b, j0);
    for (size_t idx = 1; idx < exponents_a.size(); ++idx)
        exponents_a[idx] = exponents_a[idx - 1] * exp_a;
    for (size_t idx = 1; idx < exponents_b.size(); ++idx)
        exponents_b[idx] = exponents_b[idx - 1] * exp_b;

    complex_t result(0.0, 0.0);

    if (Numeric::almostEqual(exp_c, 1., 1)) {
        // exp(i q.c) ~ 1: the k-axis phase factors are trivial, sum reduces to count.
        for (const auto& column : k_columns) {
            const complex_t e_ab = exponents_a[column.i - i0] * exponents_b[column.j - j0];
            for (const auto& k_range : column.kRanges)
                result += e_ab * (k_range.end - k_range.begin + 1.);
        }
    } else {
        // exp_c^k table and partial geometric sums S[n] = 1 + exp_c + ... + exp_c^{n-1},
        // so that sum_{k=begin}^{end} exp_c^k = exp_c^{begin} * S[count].
        std::vector<complex_t> exponents_c(shape.max.z() - k0 + 1);
        exponents_c.front() = pow(exp_c, k0);
        for (size_t idx = 1; idx < exponents_c.size(); ++idx)
            exponents_c[idx] = exponents_c[idx - 1] * exp_c;
        std::vector<complex_t> exp_c_sum_factor(shape.maxKCount + 1, complex_t(0.0, 0.0));
        complex_t exp_c_power(1.0, 0.0);
        for (int count = 1; count <= shape.maxKCount; ++count) {
            exp_c_sum_factor[count] = exp_c_sum_factor[count - 1] + exp_c_power;
            exp_c_power *= exp_c;
        }

        for (const auto& column : k_columns) {
            const complex_t e_ab = exponents_a[column.i - i0] * exponents_b[column.j - j0];
            for (const auto& k_range : column.kRanges) {
                const int count = k_range.end - k_range.begin + 1;
                result += e_ab * exponents_c[k_range.begin - k0] * exp_c_sum_factor[count];
            }
        }
    }

    return result;
}

complex_t Mesocrystal::structureAmplitude(const C3& q) const
{
    const ShapeIndexes& indexes = basisIndexes();
    return summedPhaseFactors(q, *m_lab_lattice, indexes) * debyeWallerFactor(q);
}

complex_t Mesocrystal::outerShapeFF(const WavevectorInfo& wavevectors) const
{
    WavevectorInfo wv2 = wavevectors.backTransformed(rotMatrix());
    complex_t result = m_outer_formfactor->theFF(wv2);
    if (particlePosition() != R3())
        result *= exp_I(particlePosition().dot(wavevectors.getQ()));
    return result;
}

complex_t Mesocrystal::reciprocalSpaceSum(const WavevectorInfo& wavevectors) const
{
    C3 q = wavevectors.getQ();
    double radius = m_radius_factor * m_max_rec_length;
    std::vector<R3> rec_vectors =
        m_lab_lattice->reciprocalLatticeVectorsWithinRadius(q.real(), radius);

    complex_t result(0.0, 0.0);
    for (const auto& rec : rec_vectors) {
        auto dw_factor = debyeWallerFactor(rec.complex());
        WavevectorInfo basis_wavevectors(R3(), -rec, wavevectors.vacuumLambda());
        complex_t basis_factor = m_lab_basis->theFF(basis_wavevectors);
        WavevectorInfo meso_wavevectors(C3(), rec.complex() - q, wavevectors.vacuumLambda());
        complex_t meso_factor = outerShapeFF(meso_wavevectors);
        result += dw_factor * basis_factor * meso_factor;
    }
    return result / m_lab_lattice->unitCellVolume();
}

SpinMatrix Mesocrystal::reciprocalSpaceSumPol(const WavevectorInfo& wavevectors) const
{
    C3 q = wavevectors.getQ();
    double radius = m_radius_factor * m_max_rec_length;
    std::vector<R3> rec_vectors =
        m_lab_lattice->reciprocalLatticeVectorsWithinRadius(q.real(), radius);

    SpinMatrix result;
    for (const auto& rec : rec_vectors) {
        auto dw_factor = debyeWallerFactor(rec.complex());
        WavevectorInfo basis_wavevectors(R3(), -rec, wavevectors.vacuumLambda());
        SpinMatrix basis_factor = m_lab_basis->thePolFF(basis_wavevectors);
        WavevectorInfo meso_wavevectors(C3(), rec.complex() - q, wavevectors.vacuumLambda());
        complex_t meso_factor = outerShapeFF(meso_wavevectors);
        result += dw_factor * basis_factor * meso_factor;
    }
    return result / m_lab_lattice->unitCellVolume();
}

void Mesocrystal::calculateLargestReciprocalDistance()
{
    R3 a1 = m_lab_lattice->basisVectorA();
    R3 a2 = m_lab_lattice->basisVectorB();
    R3 a3 = m_lab_lattice->basisVectorC();

    m_max_rec_length = std::max(pi / a1.mag(), pi / a2.mag());
    m_max_rec_length = std::max(m_max_rec_length, pi / a3.mag());
}

complex_t Mesocrystal::debyeWallerFactor(const C3& q) const
{
    const complex_t q2 = q.analytic_square(); // qx²+qy²+qz², no conjugation
    const double sigma = m_crystal->position_variance();
    return std::exp(-q2 * sigma * sigma / 2.0);
}
