Skip to content

Commit

Permalink
Refactor and test Structure Factor (#4205)
Browse files Browse the repository at this point in the history
Fixes #270, closes #4182

Description of changes:
- fix undefined behavior (access out of bounds)
- simplify structure factor code: use an exception instead of `errexit()`, combine the two C++ functions into a single function
- check structure factor against analytical solutions for simple tetragonal lattices
  • Loading branch information
kodiakhq[bot] authored Apr 8, 2021
2 parents d9b112e + f8e9a40 commit e700b1b
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 74 deletions.
100 changes: 41 additions & 59 deletions src/core/statistics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
#include <utils/contains.hpp>
#include <utils/math/sqr.hpp>

#include <cassert>
#include <cstdlib>
#include <limits>
#include <stdexcept>

/****************************************************************************************
* basic observables calculation
Expand Down Expand Up @@ -263,84 +265,64 @@ void calc_part_distribution(PartCfg &partCfg, std::vector<int> const &p1_types,
dist[i] /= (double)cnt;
}

std::vector<double> calc_structurefactor(PartCfg &partCfg,
std::vector<int> const &p_types,
int order) {
auto const order2 = order * order;
std::vector<double> ff;
ff.resize(2 * order2);
ff[2 * order2] = 0;
void calc_structurefactor(PartCfg &partCfg, std::vector<int> const &p_types,
int order, std::vector<double> &wavevectors,
std::vector<double> &intensities) {

if (order < 1)
throw std::domain_error("order has to be a strictly positive number");

auto const order_sq = order * order;
std::vector<double> ff(2 * order_sq + 1);
auto const twoPI_L = 2 * Utils::pi() / box_geo.length()[0];

if (order < 1) {
fprintf(stderr,
"WARNING: parameter \"order\" has to be a whole positive number");
fflush(nullptr);
errexit();
} else {
for (int qi = 0; qi < 2 * order2; qi++) {
ff[qi] = 0.0;
}
for (int i = 0; i <= order; i++) {
for (int j = -order; j <= order; j++) {
for (int k = -order; k <= order; k++) {
auto const n = i * i + j * j + k * k;
if ((n <= order2) && (n >= 1)) {
double C_sum = 0.0, S_sum = 0.0;
for (auto const &p : partCfg) {
for (int t : p_types) {
if (p.p.type == t) {
auto const qr =
twoPI_L * (Utils::Vector3i{{i, j, k}} * p.r.p);
C_sum += cos(qr);
S_sum += sin(qr);
}
for (int i = 0; i <= order; i++) {
for (int j = -order; j <= order; j++) {
for (int k = -order; k <= order; k++) {
auto const n = i * i + j * j + k * k;
if ((n <= order_sq) && (n >= 1)) {
double C_sum = 0.0, S_sum = 0.0;
for (auto const &p : partCfg) {
for (int t : p_types) {
if (p.p.type == t) {
auto const qr = twoPI_L * (Utils::Vector3i{{i, j, k}} * p.r.p);
C_sum += cos(qr);
S_sum += sin(qr);
}
}
ff[2 * n - 2] += C_sum * C_sum + S_sum * S_sum;
ff[2 * n - 1]++;
}
ff[2 * n - 2] += C_sum * C_sum + S_sum * S_sum;
ff[2 * n - 1]++;
}
}
}
int n = 0;
for (auto const &p : partCfg) {
for (int t : p_types) {
if (p.p.type == t)
n++;
}
}

long n_particles = 0l;
for (auto const &p : partCfg) {
for (int t : p_types) {
if (p.p.type == t)
n_particles++;
}
for (int qi = 0; qi < order2; qi++)
if (ff[2 * qi + 1] != 0)
ff[2 * qi] /= n * ff[2 * qi + 1];
}
return ff;
}

std::vector<std::vector<double>> modify_stucturefactor(int order,
double const *sf) {
int length = 0;

for (int i = 0; i < order * order; i++) {
if (sf[2 * i + 1] > 0) {
for (int qi = 0; qi < order_sq; qi++) {
if (ff[2 * qi + 1] != 0) {
ff[2 * qi] /= static_cast<double>(n_particles) * ff[2 * qi + 1];
length++;
}
}

auto const qfak = 2.0 * Utils::pi() / box_geo.length()[0];
std::vector<double> intern;
intern.assign(2, 0.0);
std::vector<std::vector<double>> structure_factor;
structure_factor.assign(length, intern);
wavevectors.resize(length);
intensities.resize(length);

int cnt = 0;
for (int i = 0; i < order * order; i++) {
if (sf[2 * i + 1] > 0) {
structure_factor[cnt][0] = qfak * sqrt(i + 1);
structure_factor[cnt][1] = sf[2 * i];
for (int i = 0; i < order_sq; i++) {
if (ff[2 * i + 1] != 0) {
wavevectors[cnt] = twoPI_L * sqrt(i + 1);
intensities[cnt] = ff[2 * i];
cnt++;
}
}

return structure_factor;
}
17 changes: 8 additions & 9 deletions src/core/statistics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,15 @@ void calc_part_distribution(PartCfg &partCfg, std::vector<int> const &p1_types,
* and sf[1]=1. For q=7, there are no possible wave vectors, so
* sf[2*(7-1)]=sf[2*(7-1)+1]=0.
*
* @param partCfg particle collection
* @param p_types list with types of particles to be analyzed
* @param order the maximum wave vector length in 2PI/L
* @param[in] partCfg particle collection
* @param[in] p_types list with types of particles to be analyzed
* @param[in] order the maximum wave vector length in units of 2PI/L
* @param[out] wavevectors the scattering vectors q
* @param[out] intensities the structure factor S(q)
*/
std::vector<double> calc_structurefactor(PartCfg &partCfg,
std::vector<int> const &p_types,
int order);

std::vector<std::vector<double>> modify_stucturefactor(int order,
double const *sf);
void calc_structurefactor(PartCfg &partCfg, std::vector<int> const &p_types,
int order, std::vector<double> &wavevectors,
std::vector<double> &intensities);

/** Calculate the center of mass of a special type of the current configuration.
* \param part_type type of the particle
Expand Down
3 changes: 1 addition & 2 deletions src/python/espressomd/analyze.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ cdef extern from "Observable_stat.hpp":
size_t chunk_size()

cdef extern from "statistics.hpp":
cdef vector[double] calc_structurefactor(PartCfg & , const vector[int] & p_types, int order)
cdef vector[vector[double]] modify_stucturefactor(int order, double * sf)
cdef void calc_structurefactor(PartCfg & , const vector[int] & p_types, int order, vector[double] & wavevectors, vector[double] & intensities) except +
cdef double mindist(PartCfg & , const vector[int] & set1, const vector[int] & set2)
cdef vector[int] nbhood(PartCfg & , const Vector3d & pos, double r_catch, const Vector3i & planedims)
cdef vector[double] calc_linear_momentum(int include_particles, int include_lbfluid)
Expand Down
10 changes: 6 additions & 4 deletions src/python/espressomd/analyze.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -629,15 +629,17 @@ class Analysis:
"""

if (sf_types is None) or (not hasattr(sf_types, '__iter__')):
if sf_types is None or not hasattr(sf_types, '__iter__'):
raise ValueError("sf_types has to be a list!")
check_type_or_throw_except(
sf_order, 1, int, "sf_order has to be an int!")

sf = analyze.calc_structurefactor(
analyze.partCfg(), sf_types, sf_order)
cdef vector[double] wavevectors
cdef vector[double] intensities
analyze.calc_structurefactor(
analyze.partCfg(), sf_types, sf_order, wavevectors, intensities)

return np.transpose(analyze.modify_stucturefactor(sf_order, sf.data()))
return np.vstack([wavevectors, intensities])

#
# distribution
Expand Down
1 change: 1 addition & 0 deletions testsuite/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ python_test(FILE hat.py MAX_NUM_PROC 4)
python_test(FILE analyze_energy.py MAX_NUM_PROC 2)
python_test(FILE analyze_mass_related.py MAX_NUM_PROC 4)
python_test(FILE rdf.py MAX_NUM_PROC 1)
python_test(FILE sf_simple_lattice.py MAX_NUM_PROC 1)
python_test(FILE coulomb_mixed_periodicity.py MAX_NUM_PROC 4)
python_test(FILE coulomb_cloud_wall_duplicated.py MAX_NUM_PROC 4 LABELS gpu)
python_test(FILE collision_detection.py MAX_NUM_PROC 4)
Expand Down
181 changes: 181 additions & 0 deletions testsuite/python/sf_simple_lattice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#
# Copyright (C) 2017-2021 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 byss
# 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 <http://www.gnu.org/licenses/>.
#

import unittest as ut
import espressomd
import numpy as np
import itertools


class StructureFactorTest(ut.TestCase):
'''
Test structure factor analysis against rectangular lattices.
We do not check the wavevectors directly, but rather the
corresponding SF order, which is more readable (integer value).
'''

box_l = 16
part_ty = 0
sf_order = 16
system = espressomd.System(box_l=[box_l, box_l, box_l])

def tearDown(self):
self.system.part.clear()

def order(self, wavevectors, a):
"""
Square and rescale wavevectors to recover the corresponding
SF order, which is an integer between 0 and ``self.sf_order``.
"""
return (wavevectors * a / (2 * np.pi))**2

def generate_peaks(self, a, b, c, conditions):
'''
Generate the main diffraction peaks for crystal structures.
Parameters
----------
a: :obj:`float`
Length of the unit cell on the x-axis.
b: :obj:`float`
Length of the unit cell on the y-axis.
c: :obj:`float`
Length of the unit cell on the z-axis.
conditions: :obj:`function`
Reflection conditions for the crystal lattice.
'''
hkl_ranges = [
range(0, self.sf_order + 1),
range(-self.sf_order, self.sf_order + 1),
range(-self.sf_order, self.sf_order + 1),
]
reflections = [np.linalg.norm([h / a, k / b, l / c]) * 2 * np.pi
for (h, k, l) in itertools.product(*hkl_ranges)
if conditions(h, k, l) and (h + k + l != 0) and
(h**2 + k**2 + l**2) <= self.sf_order**2]
return np.unique(reflections)

def test_tetragonal(self):
"""Check tetragonal lattice."""
a = 2
b = 4
c = 8
xen = range(0, self.box_l, a)
yen = range(0, self.box_l, b)
zen = range(0, self.box_l, c)
for i, j, k in itertools.product(xen, yen, zen):
self.system.part.add(type=self.part_ty, pos=(i, j, k))
wavevectors, intensities = self.system.analysis.structure_factor(
sf_types=[self.part_ty], sf_order=self.sf_order)
intensities = np.around(intensities, 8)
# no reflection conditions on (h,k,l)
peaks_ref = self.generate_peaks(a, b, c, lambda h, k, l: True)
peaks = wavevectors[np.nonzero(intensities)]
np.testing.assert_array_almost_equal(
self.order(peaks, a), self.order(peaks_ref[:len(peaks)], a))

def test_sc(self):
"""Check simple cubic lattice."""
l0 = 4
xen = range(0, self.box_l, l0)
for i, j, k in itertools.product(xen, repeat=3):
self.system.part.add(type=self.part_ty, pos=(i, j, k))
wavevectors, intensities = self.system.analysis.structure_factor(
sf_types=[self.part_ty], sf_order=self.sf_order)
intensities = np.around(intensities, 8)
np.testing.assert_array_equal(
intensities[np.nonzero(intensities)], len(self.system.part))
# no reflection conditions on (h,k,l)
peaks = wavevectors[np.nonzero(intensities)]
peaks_ref = self.generate_peaks(l0, l0, l0, lambda h, k, l: True)
np.testing.assert_array_almost_equal(
self.order(peaks, l0), self.order(peaks_ref[:len(peaks)], l0))

def test_bcc(self):
"""Check body-centered cubic lattice."""
l0 = 4
m = l0 / 2
xen = range(0, self.box_l, l0)
for i, j, k in itertools.product(xen, repeat=3):
self.system.part.add(type=self.part_ty, pos=(i, j, k))
self.system.part.add(type=self.part_ty, pos=(i + m, j + m, k + m))
wavevectors, intensities = self.system.analysis.structure_factor(
sf_types=[self.part_ty], sf_order=self.sf_order)
intensities = np.around(intensities, 8)
np.testing.assert_array_equal(
intensities[np.nonzero(intensities)], len(self.system.part))
# reflection conditions
# (h+k+l) even => F = 2f, otherwise F = 0
peaks_ref = self.generate_peaks(
l0, l0, l0, lambda h, k, l: (h + k + l) % 2 == 0)
peaks = wavevectors[np.nonzero(intensities)]
np.testing.assert_array_almost_equal(
self.order(peaks, l0), self.order(peaks_ref[:len(peaks)], l0))

def test_fcc(self):
"""Check face-centered cubic lattice."""
l0 = 4
m = l0 / 2
xen = range(0, self.box_l, l0)
for i, j, k in itertools.product(xen, repeat=3):
self.system.part.add(type=self.part_ty, pos=(i, j, k))
self.system.part.add(type=self.part_ty, pos=(i + m, j + m, k))
self.system.part.add(type=self.part_ty, pos=(i + m, j, k + m))
self.system.part.add(type=self.part_ty, pos=(i, j + m, k + m))
wavevectors, intensities = self.system.analysis.structure_factor(
sf_types=[self.part_ty], sf_order=self.sf_order)
intensities = np.around(intensities, 8)
np.testing.assert_array_equal(
intensities[np.nonzero(intensities)], len(self.system.part))
# reflection conditions
# (h,k,l) all even or odd => F = 4f, otherwise F = 0
peaks_ref = self.generate_peaks(
l0, l0, l0, lambda h, k, l:
h % 2 == 0 and k % 2 == 0 and l % 2 == 0 or
h % 2 == 1 and k % 2 == 1 and l % 2 == 1)
peaks = wavevectors[np.nonzero(intensities)]
np.testing.assert_array_almost_equal(
self.order(peaks, l0), self.order(peaks_ref[:len(peaks)], l0))

def test_cco(self):
"""Check c-centered orthorhombic lattice."""
l0 = 4
m = l0 / 2
xen = range(0, self.box_l, l0)
for i, j, k in itertools.product(xen, repeat=3):
self.system.part.add(type=self.part_ty, pos=(i, j, k))
self.system.part.add(type=self.part_ty, pos=(i + m, j + m, k))
wavevectors, intensities = self.system.analysis.structure_factor(
sf_types=[self.part_ty], sf_order=self.sf_order)
intensities = np.around(intensities, 8)
# reflection conditions
# (h+k) even => F = 2f, otherwise F = 0
peaks_ref = self.generate_peaks(
l0, l0, l0, lambda h, k, l: (h + k) % 2 == 0)
peaks = wavevectors[np.nonzero(intensities)]
np.testing.assert_array_almost_equal(
self.order(peaks, l0), self.order(peaks_ref[:len(peaks)], l0))

def test_exceptions(self):
with self.assertRaisesRegex(ValueError, 'order has to be a strictly positive number'):
self.system.analysis.structure_factor(sf_types=[0], sf_order=0)


if __name__ == "__main__":
ut.main()

0 comments on commit e700b1b

Please sign in to comment.