-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathWoStLaplace2D.cpp
226 lines (201 loc) · 8.88 KB
/
WoStLaplace2D.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// (Slow) implementation of the "Walk on Stars" algorithm for Laplace equations.
// Corresponds to the estimator given in Equation 18 of Sawhney et al,
// "Walk on Stars: A Grid-Free Monte Carlo Method for PDEs with Neumann Boundary
// Conditions" (2023), assuming no source term and zero-Neumann conditions.
// NOTE: this code makes a few shortcuts for the sake of code brevity; may
// be more suitable for tutorials than for production code/evaluation.
// To compile: c++ -std=c++17 -O3 -pedantic -Wall WoStLaplace2D.cpp -o wost
#include <algorithm>
#include <array>
#include <complex>
#include <functional>
#include <iostream>
#include <random>
#include <vector>
#include <fstream>
using namespace std;
// the constant "infinity" is used as a maximum value in several calculations
const double infinity = numeric_limits<double>::infinity();
// returns a random value in the range [rMin,rMax]
double random( double rMin, double rMax ) {
const double rRandMax = 1.0/(double)RAND_MAX;
double u = rRandMax*(double)rand();
return u*(rMax-rMin) + rMin;
}
// use std::complex to implement 2D vectors
using Vec2D = complex<double>;
double length( Vec2D u ) { return sqrt( norm(u) ); }
double angleOf(Vec2D u) { return arg(u); }
Vec2D rotate90( Vec2D u ) { return Vec2D( -imag(u), real(u) ); }
double dot(Vec2D u, Vec2D v) { return real(u)*real(v) + imag(u)*imag(v); }
double cross(Vec2D u, Vec2D v) { return real(u)*imag(v) - imag(u)*real(v); }
// returns the closest point to x on a segment with endpoints a and b
Vec2D closestPoint( Vec2D x, Vec2D a, Vec2D b ) {
Vec2D u = b-a;
double t = clamp( dot(x-a,u)/dot(u,u), 0.0, 1.0 );
return (1.0-t)*a + t*b;
}
// returns true if the point b on the polyline abc is a silhoutte relative to x
bool isSilhouette( Vec2D x, Vec2D a, Vec2D b, Vec2D c ) {
return cross(b-a,x-a) * cross(c-b,x-b) < 0;
}
// returns the time t at which the ray x+tv intersects segment ab,
// or infinity if there is no intersection
double rayIntersection( Vec2D x, Vec2D v, Vec2D a, Vec2D b ) {
Vec2D u = b - a;
Vec2D w = x - a;
double d = cross(v,u);
double s = cross(v,w) / d;
double t = cross(u,w) / d;
if (t > 0. && 0. <= s && s <= 1.) {
return t;
}
return infinity;
}
// boundary geometry is represented by polylines
using Polyline = vector<Vec2D>;
// returns distance from x to closest point on the given polylines P
double distancePolylines( Vec2D x, const vector<Polyline>& P ) {
double d = infinity; // minimum distance so far
for( int i = 0; i < P.size(); i++ ) { // iterate over polylines
for( int j = 0; j < P[i].size()-1; j++ ) { // iterate over segments
Vec2D y = closestPoint( x, P[i][j], P[i][j+1] ); // distance to segment
d = min( d, length(x-y) ); // update minimum distance
}
}
return d;
}
// returns distance from x to closest silhouette point on the given polylines P
double silhouetteDistancePolylines( Vec2D x, const vector<Polyline>& P ){
double d = infinity; // minimum distance so far
for( int i = 0; i < P.size(); i++ ) { // iterate over polylines
for( int j = 1; j < P[i].size()-1; j++ ) { // iterate over segment pairs
if( isSilhouette( x, P[i][j-1], P[i][j], P[i][j+1] )) {
d = min( d, length(x-P[i][j]) ); // update minimum distance
}
}
}
return d;
}
// finds the first intersection y of the ray x+tv with the given polylines P,
// restricted to a ball of radius r around x. The flag onBoundary indicates
// whether the first hit is on a boundary segment (rather than the sphere), and
// if so sets n to the normal at the hit point.
Vec2D intersectPolylines( Vec2D x, Vec2D v, double r,
const vector<Polyline>& P,
Vec2D& n, bool& onBoundary ) {
double tMin = r; // smallest hit time so far
n = Vec2D{ 0.0, 0.0 }; // first hit normal
onBoundary = false; // will be true only if the first hit is on a segment
for( int i = 0; i < P.size(); i++ ) { // iterate over polylines
for( int j = 0; j < P[i].size()-1; j++ ) { // iterate over segments
const double c = 1e-5; // ray offset (to avoid self-intersection)
double t = rayIntersection( x + c*v, v, P[i][j], P[i][j+1] );
if( t < tMin ) { // closest hit so far
tMin = t;
n = rotate90( P[i][j+1] - P[i][j] ); // get normal
n /= length(n); // make normal unit length
onBoundary = true;
}
}
}
return x + tMin*v; // first hit location
}
// solves a Laplace equation Delta u = 0 at x0, where the Dirichlet and Neumann
// boundaries are each given by a collection of polylines, the Neumann
// boundary conditions are all zero, and the Dirichlet boundary conditions
// are given by a function g that can be evaluated at any point in space
double solve( Vec2D x0, // evaluation point
vector<Polyline> boundaryDirichlet, // absorbing part of the boundary
vector<Polyline> boundaryNeumann, // reflecting part of the boundary
function<double(Vec2D)> g ) { // Dirichlet boundary values
const double eps = 0.0001; // stopping tolerance
const double rMin = 0.0001; // minimum step size
const int nWalks = 65536; // number of Monte Carlo samples
const int maxSteps = 65536; // maximum walk length
double sum = 0.0; // running sum of boundary contributions
for( int i = 0; i < nWalks; i++ ) {
Vec2D x = x0; // start walk at the evaluation point
Vec2D n{ 0.0, 0.0 }; // assume x0 is an interior point, and has no normal
bool onBoundary = false; // flag whether x is on the interior or boundary
double r, dDirichlet, dSilhouette; // radii used to define star shaped region
int steps = 0;
do { // loop until the walk hits the Dirichlet boundary
// compute the radius of the largest star-shaped region
dDirichlet = distancePolylines( x, boundaryDirichlet );
dSilhouette = silhouetteDistancePolylines( x, boundaryNeumann );
r = max( rMin, min( dDirichlet, dSilhouette ));
// intersect a ray with the star-shaped region boundary
double theta = random( -M_PI, M_PI );
if( onBoundary ) { // sample from a hemisphere around the normal
theta = theta/2. + angleOf(n);
}
Vec2D v{ cos(theta), sin(theta) }; // unit ray direction
x = intersectPolylines( x, v, r, boundaryNeumann, n, onBoundary );
steps++;
}
while(dDirichlet > eps && steps < maxSteps);
//stop if we hit the Dirichlet boundary, or the walk is too long
if( steps >= maxSteps ) cerr << "Hit max steps" << endl;
sum += g(x); // accumulate contribution of the boundary value
}
return sum/nWalks; // Monte Carlo estimate
}
double lines( Vec2D x ) {
const double s = 8.0;
return fmod( floor(s*real(x)), 2.0 );
}
// for simplicity, in this code we assume that the Dirichlet and Neumann
// boundary polylines form a collection of closed polygons (possibly with holes),
// and are given with consistent counter-clockwise orientation
vector<Polyline> boundaryDirichlet = {
{{ Vec2D(0.2, 0.2), Vec2D(0.6, 0.0), Vec2D(1.0, 0.2) }},
{{ Vec2D(1.0, 1.0), Vec2D(0.6, 0.8), Vec2D(0.2, 1.0) }}
};
vector<Polyline> boundaryNeumann = {
{{ Vec2D(1.0, 0.2), Vec2D(0.8, 0.6), Vec2D(1.0, 1.0) }},
{{ Vec2D(0.2, 1.0), Vec2D(0.0, 0.6), Vec2D(0.2, 0.2) }}
};
// these routines are not used by WoSt itself, but are rather used to check
// whether a given evaluation point is actually inside the domain
double signedAngle( Vec2D x, const vector<Polyline>& P )
{
double Theta = 0.;
for( int i = 0; i < P.size(); i++ )
for( int j = 0; j < P[i].size()-1; j++ )
Theta += arg( (P[i][j+1]-x)/(P[i][j]-x) );
return Theta;
}
// Returns true if the point x is contained in the region bounded by the Dirichlet
// and Neumann curves. We assume these curves form a collection of closed polygons,
// and are given in a consistent counter-clockwise winding order.
bool insideDomain( Vec2D x,
const vector<Polyline>& boundaryDirichlet,
const vector<Polyline>& boundaryNeumann )
{
double Theta = signedAngle( x, boundaryDirichlet ) +
signedAngle( x, boundaryNeumann );
const double delta = 1e-4; // numerical tolerance
return abs(Theta-2.*M_PI) < delta; // boundary winds around x exactly once
}
int main( int argc, char** argv ) {
srand( time(NULL) );
ofstream out( "out.csv" );
int s = 128; // image size
for( int j = 0; j < s; j++ )
{
cerr << "row " << j << " of " << s << endl;
for( int i = 0; i < s; i++ )
{
Vec2D x0( ((double)i+.5)/((double)s),
((double)j+.5)/((double)s) );
double u = 0.;
if( insideDomain(x0, boundaryDirichlet, boundaryNeumann) )
u = solve( x0, boundaryDirichlet, boundaryNeumann, lines );
out << u;
if( i < s-1 ) out << ",";
}
out << endl;
}
return 0;
}