Skip to content

Commit 39f76a3

Browse files
authored
Merge pull request #79 from stochasticHydroTools/dpstokes_open
Error checking to ensure net zero forces/torques for DPStokes when Z is open
2 parents c9c39c2 + 2ea46b2 commit 39f76a3

5 files changed

Lines changed: 111 additions & 24 deletions

File tree

solvers/DPStokes/extra/uammd_interface.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
*/
44
#include <memory>
55
#include <string>
6+
#include <vector_types.h>
67
namespace uammd_dpstokes {
78
// This is in order not to use any UAMMD related includes here.
89
// Instead of using uammd::real I have to re define real here.
910
#ifndef DOUBLE_PRECISION
1011
using real = float;
1112
using real3 = float3;
13+
using real4 = float4;
1214
#else
1315
using real = double;
1416
using real3 = double3;
17+
using real4 = double4;
1518
#endif
1619

1720
// This function returns either 'single' or 'double' according to the UAMMD's
@@ -39,6 +42,7 @@ struct PyParameters {
3942
// Can be either none, bottom, slit or periodic
4043
std::string mode;
4144
bool allowChangingBoxSize = false;
45+
bool allowUnsafeForces = false;
4246
};
4347

4448
class DPStokesUAMMD;

solvers/DPStokes/mobility.h

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,29 @@ in linear time. A. Hashemi et al. J. Chem. Phys. 158, 154101 (2023)
1212
#include <MobilityInterface/MobilityInterface.h>
1313
#include <MobilityInterface/random_finite_differences.h>
1414
#include <cmath>
15+
#include <thrust/transform_reduce.h>
1516
#include <vector>
1617

18+
namespace {
19+
struct vec3_to_vec4 {
20+
__device__ uammd_dpstokes::real4 operator()(const uammd_dpstokes::real3 &a) {
21+
return {a.x, a.y, a.z, abs(a.x) + abs(a.y) + abs(a.z)};
22+
}
23+
};
24+
25+
uammd_dpstokes::real3 calculate_relative_vec3(const uammd_dpstokes::real3 *vptr,
26+
uint numberParticles) {
27+
uammd_dpstokes::real4 sum_v = thrust::transform_reduce(
28+
thrust::cuda::par, vptr, vptr + numberParticles, vec3_to_vec4(),
29+
uammd_dpstokes::real4{0, 0, 0, 0},
30+
cuda::std::plus<uammd_dpstokes::real4>());
31+
uammd_dpstokes::real3 rel_v =
32+
uammd_dpstokes::real3{sum_v.x, sum_v.y, sum_v.z} / sum_v.w;
33+
34+
return rel_v;
35+
}
36+
} // namespace
37+
1738
class DPStokes : public libmobility::Mobility {
1839
using periodicity_mode = libmobility::periodicity_mode;
1940
using Configuration = libmobility::Configuration;
@@ -54,6 +75,7 @@ class DPStokes : public libmobility::Mobility {
5475
}
5576

5677
void initialize(Parameters ipar) override {
78+
this->par.includeAngular = ipar.includeAngular;
5779
this->dppar.viscosity = ipar.viscosity;
5880
this->lanczosTolerance = ipar.tolerance;
5981
this->dppar.mode = this->wallmode;
@@ -183,6 +205,40 @@ class DPStokes : public libmobility::Mobility {
183205
device_adapter<real> linear(ilinear, device::cuda);
184206
device_adapter<real> angular(iangular, device::cuda);
185207

208+
if (this->wallmode == "nowall" && !this->dppar.allowUnsafeForces) {
209+
constexpr real errTol = 1e-6;
210+
bool errFlag = false;
211+
if (iforces.size() > 0) {
212+
const uammd_dpstokes::real3 *fptr =
213+
reinterpret_cast<const uammd_dpstokes::real3 *>(forces.data());
214+
uammd_dpstokes::real3 rel_f =
215+
calculate_relative_vec3(fptr, this->numberParticles);
216+
if (fabs(rel_f.x) > errTol || fabs(rel_f.y) > errTol ||
217+
fabs(rel_f.z) > errTol) {
218+
errFlag = true;
219+
}
220+
}
221+
222+
if (this->par.includeAngular && itorques.size() > 0) {
223+
const uammd_dpstokes::real3 *tptr =
224+
reinterpret_cast<const uammd_dpstokes::real3 *>(torques.data());
225+
uammd_dpstokes::real3 rel_t =
226+
calculate_relative_vec3(tptr, this->numberParticles);
227+
if (fabs(rel_t.x) > errTol || fabs(rel_t.y) > errTol ||
228+
fabs(rel_t.z) > errTol) {
229+
errFlag = true;
230+
}
231+
}
232+
if (errFlag) {
233+
throw std::runtime_error(
234+
"[DPStokes] If using an open boundary in z, the mean force and "
235+
"torque in each dimension must be zero or the solver will produce "
236+
"non-physical results. If you are sure your forces and torques "
237+
"satisfy this condition, you can set allowUnsafeForces=True to "
238+
"bypass this check.");
239+
}
240+
}
241+
186242
dpstokes->Mdot(forces.data(), torques.data(), linear.data(), angular.data(),
187243
this->getNumberParticles(), this->getIncludeAngular());
188244
}
@@ -228,12 +284,12 @@ class DPStokes : public libmobility::Mobility {
228284
this->setPositions(original_pos);
229285
thrust::transform(thrust::cuda::par, thermal_drift_m.begin(),
230286
thermal_drift_m.end(), linear.begin(), linear.begin(),
231-
thrust::plus<real>());
287+
cuda::std::plus<real>());
232288
if (this->getIncludeAngular()) {
233289
device_adapter<real> angular(iangular, device::cuda);
234290
thrust::transform(thrust::cuda::par, thermal_drift_d.begin(),
235291
thermal_drift_d.end(), angular.begin(), angular.begin(),
236-
thrust::plus<real>());
292+
cuda::std::plus<real>());
237293
}
238294
}
239295

solvers/DPStokes/python_wrapper.cu

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ zmax : float
2323
allowChangingBoxSize : bool
2424
Whether the periodic extents Lx & Ly can be modified during parameter selection. Default: false.
2525
delta : float
26-
The finite difference step size for random finite differences. Specified in units of hydrodynamicRadius. Default is 1e-3.
26+
The finite difference step size for random finite differences. Specified in units of hydrodynamicRadius. Default: 1e-3.
27+
allowUnsafeForces : bool
28+
When the Z periodicity is `open` the net force/torque on the domain must be zero. Numerically, this is checked to some tolerance. Enabling this flag will by bypass the check but can lead to unphysical results if used incorrectly. Default: false.
2729
)pbdoc";
2830

2931
static const char *docstring = R"pbdoc(
@@ -41,16 +43,18 @@ MOBILITY_PYTHONIFY_WITH_EXTRA_CODE(
4143
solver.def(
4244
"setParameters",
4345
[](DPStokes &self, real Lx, real Ly, real zmin, real zmax,
44-
bool allowChangingBoxSize, real delta) {
46+
bool allowChangingBoxSize, real delta, bool allowUnsafeForces) {
4547
DPStokesParameters params;
4648
params.Lx = Lx;
4749
params.Ly = Ly;
4850
params.zmin = zmin;
4951
params.zmax = zmax;
5052
params.allowChangingBoxSize = allowChangingBoxSize;
5153
params.delta = delta;
54+
params.allowUnsafeForces = allowUnsafeForces;
5255
self.setParametersDPStokes(params);
5356
},
5457
"Lx"_a, "Ly"_a, "zmin"_a, "zmax"_a, "allowChangingBoxSize"_a = false,
55-
"delta"_a = 1e-3, setparameters_docstring);
58+
"delta"_a = 1e-3, "allowUnsafeForces"_a = false,
59+
setparameters_docstring);
5660
, docstring);

tests/test_dpstokes.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import numpy as np
22
import libMobility as lm
3+
import pytest
34

45

5-
def compute_with_dpstokes(pos, lx, ly, forces=None, torques=None):
6+
def compute_with_dpstokes(pos, lx, ly, forces=None, torques=None) -> np.ndarray:
67
params = {"viscosity": 1 / (6 * np.pi), "hydrodynamicRadius": 1.73}
78
dpstokes = lm.DPStokes("periodic", "periodic", "single_wall")
89
torques_on = True if torques is not None else False
@@ -11,9 +12,9 @@ def compute_with_dpstokes(pos, lx, ly, forces=None, torques=None):
1112
dpstokes.setPositions(pos)
1213
mf_dp, mt_dp = dpstokes.Mdot(forces=forces, torques=torques)
1314
if torques_on:
14-
return mt_dp
15+
return np.array(mt_dp)
1516
else:
16-
return mf_dp
17+
return np.array(mf_dp)
1718

1819

1920
def test_non_square_box():
@@ -34,28 +35,21 @@ def test_non_square_box():
3435
# This must be identical than doubling the box size in x and repeating the particles
3536
pos = np.vstack((pos_or, pos_or + np.array([0.5 * L_long, 0.0, 0.0])))
3637
forces = np.vstack((forces_or, forces_or))
37-
mf_dp_lx = compute_with_dpstokes(pos, forces=forces, lx=L_long, ly=L_short)[
38-
: len(pos_or), :
39-
]
40-
mt_dp_lx = compute_with_dpstokes(pos, torques=forces, lx=L_long, ly=L_short)[
41-
: len(pos_or), :
42-
]
38+
mf_dp_lx = compute_with_dpstokes(pos, forces=forces, lx=L_long, ly=L_short)
39+
mt_dp_lx = compute_with_dpstokes(pos, torques=forces, lx=L_long, ly=L_short)
4340

4441
# And the same for doubling the box size in y
4542
pos = np.vstack((pos_or, pos_or + np.array([0.0, 0.5 * L_long, 0.0])))
4643
forces = np.vstack((forces_or, forces_or))
47-
mf_dp_ly = compute_with_dpstokes(pos, forces=forces, lx=L_short, ly=L_long)[
48-
: len(pos_or), :
49-
]
50-
mt_dp_ly = compute_with_dpstokes(pos, torques=forces, lx=L_short, ly=L_long)[
51-
: len(pos_or), :
52-
]
44+
mf_dp_ly = compute_with_dpstokes(pos, forces=forces, lx=L_short, ly=L_long)
45+
mt_dp_ly = compute_with_dpstokes(pos, torques=forces, lx=L_short, ly=L_long)
5346

54-
assert np.allclose(mf_dp_lx, mf_dp_cube, rtol=1e-3, atol=1e-2)
55-
assert np.allclose(mf_dp_ly, mf_dp_cube, rtol=1e-3, atol=1e-2)
47+
slice = np.s_[0 : len(pos_or), :]
48+
assert np.allclose(mf_dp_lx[slice], mf_dp_cube, rtol=1e-3, atol=1e-2)
49+
assert np.allclose(mf_dp_ly[slice], mf_dp_cube, rtol=1e-3, atol=1e-2)
5650

57-
assert np.allclose(mt_dp_lx, mt_dp_cube, rtol=1e-3, atol=1e-2)
58-
assert np.allclose(mt_dp_ly, mt_dp_cube, rtol=1e-3, atol=1e-2)
51+
assert np.allclose(mt_dp_lx[slice], mt_dp_cube, rtol=1e-3, atol=1e-2)
52+
assert np.allclose(mt_dp_ly[slice], mt_dp_cube, rtol=1e-3, atol=1e-2)
5953

6054

6155
def test_isotropy():
@@ -126,3 +120,29 @@ def test_non_square_matching_rpy():
126120
results_rpy[i + 3] += mt_j
127121

128122
assert np.allclose(results_dpstokes, results_rpy, rtol=1e-3, atol=1e-2)
123+
124+
125+
def test_dpstokes_open_errors():
126+
solver = lm.DPStokes("periodic", "periodic", "open")
127+
solver.setParameters(Lx=10.0, Ly=10.0, zmin=0.0, zmax=10.0)
128+
solver.initialize(hydrodynamicRadius=1.0, viscosity=1.0, includeAngular=True)
129+
130+
pos = np.random.uniform(0.0, 10.0, (10, 3))
131+
solver.setPositions(pos)
132+
133+
forces = np.random.uniform(-1.0, 1.0, (10, 3))
134+
with pytest.raises(RuntimeError, match="DPStokes"):
135+
solver.Mdot(forces=forces)
136+
with pytest.raises(RuntimeError, match="DPStokes"):
137+
solver.Mdot(torques=forces)
138+
139+
forces -= np.mean(forces, axis=0)
140+
solver.Mdot(forces=forces, torques=forces)
141+
142+
# check that we can bypass the error if we allow unsafe forces
143+
forces = np.random.uniform(-1.0, 1.0, (10, 3))
144+
solver = lm.DPStokes("periodic", "periodic", "open")
145+
solver.setParameters(Lx=10.0, Ly=10.0, zmin=0.0, zmax=10.0, allowUnsafeForces=True)
146+
solver.initialize(hydrodynamicRadius=1.0, viscosity=1.0, includeAngular=True)
147+
solver.setPositions(pos)
148+
solver.Mdot(forces=forces, torques=forces)

tests/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def get_sane_params(solverName, geom=None):
108108
params = sane_parameters["NBody_wall"].copy()
109109
else:
110110
params = sane_parameters[solverName].copy()
111+
112+
if solverName == "DPStokes" and geom == "open":
113+
params["allowUnsafeForces"] = True
111114
return params
112115

113116

0 commit comments

Comments
 (0)