Skip to content

Commit 11320c4

Browse files
authored
Electricpotential analysis (#379)
Analysis for sampling the electric potential at arbitrary points. Related to collaboration with Lue and Curtis. * Add externalpotential analysis * Add non-overlapping random walk policy * Manual update
1 parent 53c8332 commit 11320c4

File tree

9 files changed

+301
-31
lines changed

9 files changed

+301
-31
lines changed

docs/_docs/analysis.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,45 @@ atomic species can be saved.
395395
`pqrfile` | Output PQR file (optional)
396396
`verbose=True` | If `True`, add results to general output
397397

398+
### Electric Potential
399+
400+
`electricpotential` | Description
401+
--------------------- | ------------------------------------------------------
402+
`nstep` | Interval between samples
403+
`nskip=0` | Number of initial steps excluded from the analysis
404+
`epsr` | Dielectric constant
405+
`type` | Coulomb type, `plain` etc. -- see energies
406+
`structure` | Either a _filename_ (pqr, aam, gro etc) or a _list_ of positions
407+
`policy=fixed` | Policy used to augment positions before each sample event, see below
408+
`ncalc` | Number of potential calculations per sample event
409+
`stride` | Separation between target points when using `random_walk` or `no_overlap`
410+
411+
This calculates the mean electric potential, $\langle \phi\_i \rangle$ and correlations, $\langle \phi\_1\phi\_2 ...\rangle$
412+
at an arbitrary number of target positions in the simulation cell.
413+
The positions, given via `structure`, can be augmented using a `policy`:
414+
415+
`policy` | Description
416+
--------------- | ------------------------------------------------------------------
417+
`fixed` | Expects a list of fixed positions where the potential is measured
418+
`random_walk` | Assign random position to first target; while following targets are randomly placed `stride` distance from previous.
419+
`no_overlap` | As `random_walk` but with no particle overlap (size defined by `sigma`, see Topology)
420+
421+
Histograms of the correlation and the potentials at the target points are saved to disk.
422+
423+
Example:
424+
425+
~~~ yaml
426+
- electricpotential:
427+
nstep: 20
428+
ncalc: 10
429+
epsr: 80
430+
type: plain
431+
policy: random_walk
432+
stride: 10 # in angstrom
433+
structure:
434+
- [0,0,0] # defines two target points...
435+
- [0,0,0] # ...positions are randomly set
436+
~~~
398437

399438
## Reaction Coordinate
400439

docs/schema.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,36 @@ properties:
905905
dr: {type: number, description: "Distance resolution along R (Å)"}
906906
required: [molecules, nstep, dr]
907907

908+
electricpotential:
909+
type: object
910+
properties:
911+
epsr: {type: number, description: "Relative dielectric constant"}
912+
type: {type: string, description: "Coulomb potential type"}
913+
stride: {type: number, description: "Stride length for random walk"}
914+
policy:
915+
description: "Policy used to augment positions before each sample event"
916+
type: string
917+
enum: ["fixed", "random_walk", "no_overlap"]
918+
default: "fixed"
919+
ncalc: {type: integer, description: "Number of samples per event", default: 1, minimum: 1}
920+
nstep: {type: integer, description: "Sample interval"}
921+
nskip: {type: integer, default: 0, description: "Initial steps to skip"}
922+
structure:
923+
description: Target position definitions
924+
anyOf:
925+
- type: string
926+
pattern: "(.*?)\\.(pqr|aam|xyz|gro)$"
927+
- type: array
928+
items:
929+
type: array
930+
additionalProperties:
931+
type: array
932+
items: {type: number}
933+
minItems: 3
934+
maxItems: 3
935+
required: [nstep, epsr, type, structure]
936+
additionalProperties: false
937+
908938
chargefluctuations:
909939
type: object
910940
properties:

src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ set(hdrs analysis.h average.h atomdata.h auxiliary.h bonds.h celllist.h celllist
6868
aux/eigen_cerealisation.h aux/eigensupport.h aux/iteratorsupport.h aux/multimatrix.h
6969
aux/eigen_cerealisation.h aux/eigensupport.h aux/equidistant_table.h aux/error_function.h
7070
aux/exp_function.h aux/invsqrt_function.h aux/iteratorsupport.h aux/legendre.h aux/multimatrix.h
71-
aux/pairmatrix.h aux/pow_function.h aux/table_1d.h aux/table_2d.h aux/timers.h aux/typeerasure.h)
71+
aux/pairmatrix.h aux/pow_function.h aux/table_1d.h aux/table_2d.h aux/timers.h aux/typeerasure.h aux/sparsehistogram.h)
7272

7373
set_source_files_properties(${objs} PROPERTIES LANGUAGE CXX)
7474

src/analysis.cpp

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "energy.h"
44
#include "reactioncoordinate.h"
55
#include "multipole.h"
6+
#include "potentials.h"
67
#include "aux/iteratorsupport.h"
78
#include "aux/eigensupport.h"
89
#include <range/v3/numeric/accumulate.hpp>
@@ -11,6 +12,7 @@
1112
#include <zstr.hpp>
1213
#include <cereal/types/memory.hpp>
1314
#include <cereal/archives/binary.hpp>
15+
#include <range/v3/numeric/accumulate.hpp>
1416

1517
#include <iomanip>
1618
#include <iostream>
@@ -114,6 +116,8 @@ std::shared_ptr<Analysisbase> createAnalysis(const std::string& name, const json
114116
return std::make_shared<AtomDipDipCorr>(j, spc);
115117
} else if (name == "density") {
116118
return std::make_shared<Density>(j, spc);
119+
} else if (name == "electricpotential") {
120+
return std::make_shared<ElectricPotential>(j, spc);
117121
} else if (name == "chargefluctuations") {
118122
return std::make_shared<ChargeFluctuations>(j, spc);
119123
} else if (name == "molrdf") {
@@ -1776,4 +1780,145 @@ void SpaceTrajectory::_sample() {
17761780
void SpaceTrajectory::_to_json(json& j) const { j = {{"file", filename}}; }
17771781

17781782
void SpaceTrajectory::_to_disk() { stream->flush(); }
1783+
1784+
// -----------------------------
1785+
1786+
ElectricPotential::ElectricPotential(const json& j, Space& spc)
1787+
: Analysisbase(spc, "electricpotential"), potential_correlation_histogram(histogram_resolution) {
1788+
from_json(j);
1789+
coulomb = std::make_shared<Potential::NewCoulombGalore>();
1790+
coulomb->from_json(j);
1791+
getTargets(j);
1792+
setPolicy(j);
1793+
calculations_per_sample_event = j.value("ncalc", 1);
1794+
}
1795+
1796+
void ElectricPotential::setPolicy(const json& j) {
1797+
output_information.clear();
1798+
policy = j.value("policy", FIXED);
1799+
auto stride = 0.0;
1800+
switch (policy) {
1801+
case FIXED:
1802+
applyPolicy = []() {};
1803+
break;
1804+
case RANDOM_WALK:
1805+
stride = j.at("stride").get<double>();
1806+
output_information["stride"] = stride;
1807+
applyPolicy = [&, stride] {
1808+
auto origin = targets.begin();
1809+
spc.geo.randompos(origin->position, random);
1810+
std::for_each(std::next(origin), targets.end(), [&](Target& target) {
1811+
target.position = origin->position + stride * ranunit(random);
1812+
std::advance(origin, 1);
1813+
});
1814+
};
1815+
break;
1816+
case RANDOM_WALK_NO_OVERLAP:
1817+
stride = j.at("stride").get<double>();
1818+
output_information["stride"] = stride;
1819+
applyPolicy = [&, stride] {
1820+
auto origin = targets.begin();
1821+
do {
1822+
spc.geo.randompos(origin->position, random);
1823+
} while (overlapWithParticles(origin->position));
1824+
std::for_each(std::next(origin), targets.end(), [&](Target& target) {
1825+
do {
1826+
target.position = origin->position + stride * ranunit(random);
1827+
} while (overlapWithParticles(target.position));
1828+
std::advance(origin, 1);
1829+
});
1830+
};
1831+
break;
1832+
default:
1833+
throw ConfigurationError("unknown policy");
1834+
}
1835+
}
1836+
1837+
void ElectricPotential::getTargets(const json& j) {
1838+
if (const auto& structure = j.find("structure"); structure == j.end()) {
1839+
throw ConfigurationError("missing structure");
1840+
} else {
1841+
PointVector positions;
1842+
if (structure->is_string()) { // load positions from chemical structure file
1843+
auto particles = loadStructure(structure->get<std::string>(), false);
1844+
positions = particles | ranges::cpp20::views::transform(&Particle::pos) | ranges::to_vector;
1845+
} else if (structure->is_array()) { // list of positions
1846+
positions = structure->get<PointVector>();
1847+
}
1848+
std::transform(positions.begin(), positions.end(), std::back_inserter(targets), [&](auto& position) {
1849+
Target target;
1850+
target.position = position;
1851+
target.potential_histogram = std::make_shared<SparseHistogram<double>>(histogram_resolution);
1852+
return target;
1853+
});
1854+
if (targets.empty()) {
1855+
throw ConfigurationError("no targets defined");
1856+
}
1857+
}
1858+
}
1859+
1860+
void ElectricPotential::_sample() {
1861+
for (unsigned int i = 0; i < calculations_per_sample_event; i++) {
1862+
applyPolicy();
1863+
auto potential_correlation = 1.0; // phi1 * phi2 * ...
1864+
for (auto& target : targets) { // loop over each target point
1865+
auto potential = calcPotentialOnTarget(target);
1866+
target.potential_histogram->add(potential);
1867+
target.mean_potential += potential;
1868+
potential_correlation *= potential;
1869+
}
1870+
mean_potential_correlation += potential_correlation; // <phi1 * phi2 * ...>
1871+
potential_correlation_histogram.add(potential_correlation); // P(<phi1 * phi2 * ...>)
1872+
}
1873+
}
1874+
1875+
double ElectricPotential::calcPotentialOnTarget(const ElectricPotential::Target& target) {
1876+
auto potential_from_particle = [&](const Particle& particle) {
1877+
const auto distance_to_target = std::sqrt(spc.geo.sqdist(particle.pos, target.position));
1878+
return coulomb->getCoulombGalore().ion_potential(particle.charge, distance_to_target);
1879+
};
1880+
auto potentials = spc.activeParticles() | ranges::cpp20::views::transform(potential_from_particle);
1881+
return std::accumulate(potentials.begin(), potentials.end(), 0.0);
1882+
}
1883+
1884+
void ElectricPotential::_to_json(json& j) const {
1885+
j = output_information;
1886+
coulomb->to_json(j["coulomb"]);
1887+
j["policy"] = policy;
1888+
j["number of targets"] = targets.size();
1889+
j["calculations per sample"] = calculations_per_sample_event;
1890+
if (number_of_samples > 0) {
1891+
j["correlation (βe)ⁱ⟨ϕ₁ϕ₂...ϕᵢ⟩"] = mean_potential_correlation.avg();
1892+
auto& mean_potential_j = j["mean potentials βe⟨ϕᵢ⟩"] = json::array();
1893+
std::transform(targets.begin(), targets.end(), std::back_inserter(mean_potential_j),
1894+
[](const auto& target) { return target.mean_potential.avg(); });
1895+
}
1896+
}
1897+
void ElectricPotential::_to_disk() {
1898+
if (auto stream = std::ofstream(MPI::prefix + "potential_correlation_histogram.dat")) {
1899+
stream << potential_correlation_histogram;
1900+
}
1901+
int filenumber = 1;
1902+
for (const auto& target : targets) {
1903+
if (auto stream = std::ofstream(fmt::format("{}potential_histogram{}.dat", MPI::prefix, filenumber++))) {
1904+
stream << *(target.potential_histogram);
1905+
}
1906+
}
1907+
}
1908+
1909+
/**
1910+
* Checks if position lies within the spheres of diameters `sigma` defined
1911+
* by each active particle in the system. Complexity: N
1912+
*
1913+
* @return True if overlap with any particle
1914+
*/
1915+
bool ElectricPotential::overlapWithParticles(const Point& position) const {
1916+
auto overlap = [&position, &geometry = spc.geo](const Particle& particle) {
1917+
const auto radius = 0.5 * particle.traits().sigma;
1918+
return geometry.sqdist(particle.pos, position) < radius * radius;
1919+
};
1920+
auto particles = spc.activeParticles();
1921+
return std::any_of(particles.begin(), particles.end(), overlap);
1922+
}
1923+
17791924
} // namespace Faunus::Analysis

src/analysis.h

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@
77
#include "aux/timers.h"
88
#include "aux/table_2d.h"
99
#include "aux/equidistant_table.h"
10+
#include <aux/sparsehistogram.h>
1011
#include <set>
1112

1213
namespace cereal {
1314
class BinaryOutputArchive;
1415
}
1516

16-
namespace Faunus {
17+
namespace Faunus::Potential {
18+
class NewCoulombGalore;
19+
}
1720

18-
namespace Energy {
21+
namespace Faunus::Energy {
1922
class Hamiltonian;
2023
class Energybase;
21-
} // namespace Energy
24+
} // namespace Faunus::Energy
2225

26+
namespace Faunus {
2327
/**
2428
* Adding a new analysis requires the following steps:
2529
*
@@ -162,6 +166,48 @@ class WidomInsertion : public PerturbationAnalysisBase {
162166
WidomInsertion(const json& j, Space& spc, Energy::Hamiltonian& pot);
163167
};
164168

169+
/**
170+
* @brief Samples the electric potential at arbitrary positions in the simulation box
171+
* @todo Add policies for random mass-center position and orientation
172+
*/
173+
class ElectricPotential : public Analysisbase {
174+
public:
175+
enum Policies { FIXED, RANDOM_WALK, RANDOM_WALK_NO_OVERLAP, INVALID };
176+
177+
private:
178+
double histogram_resolution = 0.05; //!< Angstrom
179+
unsigned int calculations_per_sample_event = 1;
180+
struct Target {
181+
Point position; //!< Target position
182+
Average<double> mean_potential; //!< mean potential at position
183+
std::shared_ptr<SparseHistogram<double>> potential_histogram; //!< Histogram of observed potentials
184+
};
185+
std::vector<Target> targets; //!< List of target points where to sample the potential
186+
Policies policy; //!< Policy to apply to targets before each sample event
187+
std::shared_ptr<Potential::NewCoulombGalore> coulomb; //!< Class for calculating the potential
188+
Average<double> mean_potential_correlation; //!< Correlation between targets, <phi1 x phi2 x ... >
189+
SparseHistogram<double> potential_correlation_histogram; //!< Distribution of correlations, P(<phi1 x phi2 x ... >)
190+
void getTargets(const json& j); //!< Get user defined target positions
191+
void setPolicy(const json& j); //!< Set user defined position setting policy
192+
double calcPotentialOnTarget(const Target& target); //!< Evaluate net potential of target position
193+
bool overlapWithParticles(const Point& position) const; //!< Check if position is within the radius of any particle
194+
std::function<void()> applyPolicy; //!< Lambda for position setting policy
195+
json output_information; //!< json output generated during construction
196+
void _to_json(json& j) const override;
197+
void _sample() override;
198+
void _to_disk() override;
199+
200+
public:
201+
ElectricPotential(const json& j, Space& spc);
202+
};
203+
204+
NLOHMANN_JSON_SERIALIZE_ENUM(ElectricPotential::Policies, {
205+
{ElectricPotential::INVALID, nullptr},
206+
{ElectricPotential::FIXED, "fixed"},
207+
{ElectricPotential::RANDOM_WALK_NO_OVERLAP, "no_overlap"},
208+
{ElectricPotential::RANDOM_WALK, "random_walk"},
209+
})
210+
165211
/**
166212
* @brief Density of atom along axis
167213
*

src/aux/sparsehistogram.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <map>
4+
#include <fstream>
5+
6+
namespace Faunus {
7+
8+
/**
9+
* @brief Histogram for an arbitrary set of values using a sparse memory layout (map)
10+
*
11+
* Builds a histogram by binning given values to a specified resolution. Values are stored
12+
* in a memory efficient map-structure with log(N) lookup complexity.
13+
*/
14+
template <typename T = double> class SparseHistogram {
15+
T resolution;
16+
std::map<int, unsigned int> data;
17+
18+
public:
19+
explicit SparseHistogram(T resolution) : resolution(resolution) {}
20+
void add(const T value) {
21+
if (std::isfinite(value)) {
22+
data[static_cast<int>(std::round(value / resolution))]++;
23+
} else {
24+
faunus_logger->warn("histogram: skipping inf/nan number");
25+
}
26+
}
27+
friend auto& operator<<(std::ostream& stream, const SparseHistogram& histogram) {
28+
std::for_each(histogram.data.begin(), histogram.data.end(), [&](const auto& sample) {
29+
stream << fmt::format("{:.6E} {}\n", T(sample.first) * histogram.resolution, sample.second);
30+
});
31+
return stream;
32+
}
33+
};
34+
} // namespace Faunus

0 commit comments

Comments
 (0)