I have a function to generate a (pseudo) random walk on a square lattice where the walk should not breach the boundaries of this square, full function below:
/**
* #brief Performs a single random walk returning the final distance from the origin
*
* Completes a random walk on a square lattice using the mersenne twister engine based pseudo-random
* number-generator (PRNG). The walk will not breach the boundaries of the square size provided to
* the function. The random walk starts at the origin and ends after some parameterised number of steps.
* Position co-ordinates of the walk for each iteration are sent to an output file.
*
* #param squareSideLength Length of square lattice side
* #param steps Number of steps to compute random walk up to
* #param engine Mersenne Twister engine typedef (used for generating random numbers locally)
* #param distribution Default distribution of random walk
* #param outputFile [Default nullptr] Pointer to file to write co-ordinate data of random walk to
* #return final distance of the particle from the origin
*/
double randomWalkSquareLattice(int squareSideLength, int steps, std::mt19937& engine, std::uniform_real_distribution<double>& distribution, std::ofstream* outputFile = nullptr) {
// store the half-length of the square lattice
const int halfSquareLength = squareSideLength / 2;
// initialise co-ordinates to the origin
double positionX = 0.0;
double positionY = 0.0;
// assign the default distribution to distDefault
std::uniform_real_distribution<double> distDefault = distribution;
// loop over a number of iterations given by the steps parameter
for (int i = 0; i < steps; i++) {
std::cout << positionX << "\t" << positionY << std::endl;
// if the x-position of the particle is >= to positive
// half square lattice length then generate decremental
// random number (avoiding breaching the boundary)
if (positionX >= halfSquareLength) {
double offset = positionX - halfSquareLength;
std::cout << std::endl << offset << std::endl;
std::uniform_real_distribution<double> distOffset(-offset, -1.0);
positionX += distOffset(engine);
}
// else if the x-position of the particle is <= to negative
// half square lattice length then generate incremental random
// number (avoiding breaching the boundary)
else if (positionX <= -halfSquareLength) {
double offset = std::abs(positionX + halfSquareLength);
std::cout << std::endl << offset << std::endl;
std::uniform_real_distribution<double> distOffset(offset, 1.0);
positionX += distOffset(engine);
}
// else (in case where x-position of particle is not touching
// the lattice boundary) generate default random number
else {
positionX += distDefault(engine);
}
// if the y-position of the particle is >= to positive
// half square lattice length then generate decremental
// random number (avoiding breaching the boundary)
if (positionY >= halfSquareLength) {
double offset = positionY - halfSquareLength;
std::cout << std::endl << offset << std::endl;
std::uniform_real_distribution<double> distOffset(-offset, -1.0);
positionY += distOffset(engine);
}
// else if the y-position of the particle is <= to negative
// half square lattice length then generate incremental
// random number (avoiding breaching the boundary)
else if (positionY <= -halfSquareLength) {
double offset = std::abs(positionY + halfSquareLength);
std::cout << std::endl << offset << std::endl;
std::uniform_real_distribution<double> distOffset(offset, 1.0);
positionY += distOffset(engine);
}
// else (in case where y-position of particle is not touching
// the lattice boundary) generate default random number
else {
positionY += distDefault(engine);
}
// if an outputFile is supplied to the function, then write data to it
if (outputFile != nullptr) {
*outputFile << positionX << "\t" << positionY << std::endl;
}
}
// compute final distance of particle from origin
double endDistance = std::sqrt(positionX*positionX + positionY*positionY);
return endDistance;
}
Where the conditionals seen in the method prevent the walk exiting the boundaries. However, when this is called with a sufficient number of steps (so that any one of these conditionals is executed) I get an error saying:
invalid min and max arguments for uniform_real
Note that the dist I send to this function is:
std::uniform_real_distribution<double> dist(-1.0,1.0);
And so (as you can see from the values printed to the terminal) the issue is not that the offset will ever be larger than the max value given to the distOffset in any of the conditional cases.
Is the issue that I cannot give u_r_d a double value of arbitrary precision? Or is something else at play here that I am missing?
Edit: I should add that these are the values used in main():
int main(void) {
std::uniform_real_distribution<double> dist(-1.0, 1.0);
std::random_device randDevice;
std::mt19937 engine(randDevice());
//std::cout << dist(engine) << std::endl;
// Dimensions of Square Lattice
const int squareLength = 100;
// Number of Steps in Random Walk
const int nSteps = 10000;
randomWalkSquareLattice(squareLength, nSteps, engine, dist);
}
uniform_real_distribution(a,b); requires that a ≤ b.
If positionX == halfSquareLength, then,
double offset = positionX - halfSquareLength;
is the same as saying
double offset = positionX - positionX;
and offset will be zero.
This results in
std::uniform_real_distribution<double> distOffset(-0.0, -1.0);
and violates a ≤ b.
Here is the solution I came up with, seems to work for all test cases so far:
/**
* #brief Performs a single random walk returning the final distance from the origin
*
* Completes a random walk on a square lattice using the mersenne twister engine based pseudo-random
* number-generator (PRNG). The walk will not breach the boundaries of the square size provided to
* the function. The random walk starts at the origin and ends after some parameterised number of steps.
* Position co-ordinates of the walk for each iteration are sent to an output file.
*
* #param squareSideLength Length of square lattice side
* #param steps Number of steps to compute random walk up to
* #param engine Mersenne Twister engine typedef (used for generating random numbers locally)
* #param distribution Default distribution of random walk
* #param outputFile [Default nullptr] Pointer to file to write co-ordinate data of random walk to
* #return final distance of the particle from the origin
*/
double randomWalkSquareLattice(int squareSideLength, int steps, std::mt19937& engine, std::uniform_real_distribution<double>& distribution, std::ofstream* outputFile = nullptr) {
// store the half-length of the square lattice
const int halfSquareLength = squareSideLength / 2;
// initialise co-ordinates to the origin
double positionX = 0.0;
double positionY = 0.0;
// assign the default distribution to distDefault
std::uniform_real_distribution<double> distDefault = distribution;
std::uniform_real_distribution<double> distBound(0.0, 1.0);
double oS;
// loop over a number of iterations given by the steps parameter
for (int i = 0; i < steps; i++) {
//std::cout << positionX << "\t" << positionY << std::endl;
positionX += distDefault(engine);
positionY += distDefault(engine);
// if the x-position of the particle is >= to positive
// half square lattice length then generate decremental
// random number (avoiding breaching the boundary)
if (positionX >= halfSquareLength) {
oS = distBound(engine);
double offset = positionX - halfSquareLength;
double desiredOffset = -(oS + offset);
if (desiredOffset < -1.0) {
double offsetFromNegUnity = desiredOffset + 1.0;
desiredOffset -= offsetFromNegUnity;
}
positionX += desiredOffset;
}
// else if the x-position of the particle is <= to negative
// half square lattice length then generate incremental random
// number (avoiding breaching the boundary)
else if (positionX <= -halfSquareLength) {
oS = distBound(engine);
double offset = std::abs(positionX + halfSquareLength);
double desiredOffset = offset+oS;
if (desiredOffset > 1.0) {
double offsetFromUnity = desiredOffset - 1.0;
desiredOffset -= offsetFromUnity;
}
positionX += desiredOffset;
}
// if the y-position of the particle is >= to positive
// half square lattice length then generate decremental
// random number (avoiding breaching the boundary)
if (positionY >= halfSquareLength) {
oS = distBound(engine);
double offset = positionY - halfSquareLength;
double desiredOffset = -(offset+oS);
if (desiredOffset < -1.0) {
double offsetFromNegUnity = desiredOffset + 1.0;
desiredOffset -= offsetFromNegUnity;
}
positionY += desiredOffset;
}
// else if the y-position of the particle is <= to negative
// half square lattice length then generate incremental
// random number (avoiding breaching the boundary)
else if (positionY <= -halfSquareLength) {
oS = distBound(engine);
double offset = std::abs(positionY + halfSquareLength);
double desiredOffset = offset+oS;
if (desiredOffset > 1.0) {
double offsetFromUnity = desiredOffset - 1.0;
desiredOffset -= offsetFromUnity;
}
positionY += desiredOffset;
}
// if an outputFile is supplied to the function, then write data to it
if (outputFile != nullptr) {
*outputFile << positionX << "\t" << positionY << std::endl;
}
}
// compute final distance of particle from origin
double endDistance = std::sqrt(positionX*positionX + positionY*positionY);
return endDistance;
}
Here, an offset was generated randomly on the interval (0,1) and the difference from the boundary by which a x or y position breached was added to this offset to create a double value which would have a minimum of this breaching difference and (after an additional nested conditional check) a maximum of 1.0 (or -1.0 for opposite boundary).
Related
I have been reading and trying out "Ray tracing in one weekend" by Peter Shirley. Everything has been going great until the diffuse material part. Basically, instead of a diffuse material, my algorithm seems to only be casting shadows from a specific angle and I have no idea from where the problem could originate from.
I have normally been following the book step by step.
The previous sections give the correct results and the only code I have added from the last section to the diffuse material one are the functions below.
Here are the specific parts of the code for diffuse material, which basically reflect the ray into a random direction, chosen from a sphere that is tangent to the collision point (Sorry if my explanation isn't clear enough).
This is the function that take a random point from a sphere tangent to the collision point.
vec3 random_in_unitSphere(){
vec3 p;
std::default_random_engine generator;
std::uniform_real_distribution<float> distribution(0.0, 1.0);
do{
p = 2.0*vec3(distribution(generator),distribution(generator),distribution(generator)) - vec3(1,1,1);
}while (p.squared_length() >= 1.0);
return p;
}
This is the function that calculate the color of a pixel (By casting rays until it hits nothing)
vec3 color(const Ray& r,Hitable *world){
hit_record rec;
if(world->hit(r,0.0,FLT_MAX,rec)){
vec3 target = rec.p + rec.normal + random_in_unitSphere();
return 0.5*color(Ray(rec.p,target-rec.p),world);
}
else{
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*vec3(1.0,1.0,1.0) + t*vec3(0.5,0.7,1.0);
}
}
And this is the loop responsible for casting rays for every pixel of the image.
for(int j = ny-1 ; j >= 0 ; j--){
for(int i = 0; i < nx ; i++){
vec3 col(0,0,0);
for(int s = 0; s < ns ; s++){
float u = float(i+ distribution(generator)) / float(nx);
float v = float(j+ distribution(generator)) / float(ny);
Ray r = camera.getRay(u,v);
vec3 p = r.pointAt(2.0);
col += color(r,world);
}
col /= float(ns);
int ir = int (255.99*col.r());
int ig = int (255.99*col.g());
int ib = int (255.99*col.b());
outfile<< ir << " " << ig << " " << ib << std::endl;
}
}
Here is the expected output : https://imgur.com/im5HNEK
And here is what I get : https://imgur.com/heNjEVV
Thanks !
The problem is simply that every time you generate a random vector, you're using a new, default-initialized psuedorandom number generator. A random number generator contains some state, and this state needs to be preserved in order to see different results over time.
To fix this, simply make your random number generator static in one way or another:
vec3 random_in_unitSphere(){
vec3 p;
static std::default_random_engine generator{std::random_device{}()};
std::uniform_real_distribution<float> distribution(0.0, 1.0);
do{
p = 2.0*vec3(distribution(generator),distribution(generator),distribution(generator)) - vec3(1,1,1);
}while (p.squared_length() >= 1.0);
return p;
}
Here, I've also used std::random_device to (possibly) add some real-world randomness to the generator.
Random direction function looks wrong to me. It looks like it supposed to produce three directional cosines (wx, wy, wz) which are uniform on the sphere with radius=1, such that
wx2+wy2+wz2 = 1
First problem: you construct random engine each time you are entering the function, thus all your values are the same. I just put it in Visual Studio 2017, C++14.1, x64, Win10, and two calls produced
-0.383666 -0.804919 0.0944412
-0.383666 -0.804919 0.0944412
Second problem - it is not a random dimension, length is not equal to 1.
UPDATE
Following Wolfram article http://mathworld.wolfram.com/SpherePointPicking.html, here is the code which fix both problems - it does have RNG as parameter, so state would change. And second, point is now properly sampled on the unit sphere, and could be used as random direction.Just replace tuple with vec3
#include <iostream>
#include <random>
#include <tuple>
std::tuple<float,float,float> random_in_unitSphere(std::mt19937& rng) {
std::uniform_real_distribution<float> distribution{};
float x1, x2, l;
do {
x1 = 2.0f * distribution(rng) - 1.0f;
x2 = 2.0f * distribution(rng) - 1.0f;
l = x1 * x1 + x2 * x2;
} while (l >= 1.0f);
float s = sqrt(1.0f - l);
return std::make_tuple(2.0f*x1*s, 2.0f*x2*s, 1.0f - 2.0f*l);
}
int main() {
std::mt19937 rng{ 987654321ULL };
float wx, wy, wz, squared_length;
std::tie(wx, wy, wz) = random_in_unitSphere(rng);
std::cout << wx << " " << wy << " " << wz << '\n';
squared_length = wx * wx + wy * wy + wz * wz;
std::cout << squared_length << '\n';
std::tie(wx, wy, wz) = random_in_unitSphere(rng);
std::cout << wx << " " << wy << " " << wz << '\n';
squared_length = wx * wx + wy * wy + wz * wz;
std::cout << squared_length << '\n';
return 0;
}
UPDATE II
Second problem is that you generated points uniform INSIDE the unit sphere. So problem is not with directions - your wx, wy, wz are good wrt direction, but with length of you direction vector. Typical raytracing code is like that (in some pseudocode)
auto [x0,y0,z0] = start_new_ray();
auto [wx,wy,wz] = sample_direction();
float path = compute_path_in_geometry(x0,y0,z0,wx,wy,wz); // compute path from start point 0 in the wx,wy,wz direction to next object
// move ray to new surface
x1 = x0 + wx*path;
y1 = y0 + wy*path;
z1 = z0 + wz*path;
// do scattering, illumination, ... at (x1,y1,z1)
If (wx,wy,wz) length is not 1, then length computed as sqrt((x1-x0)2 + (y1-y0)2+(z1-z0)2) WON'T BE equal to path. Your basic geometry rules just breaks.
Area of the circle = Pi * R^2 and the Area of the square = 4 * R^2.
If we divide the area of the circle by the area of the square we get Pi / 4.
Let's have a square and an inscribed circle in it. We generate points with random coordinates and then count their number in each of the areas.
Then Pi = 4 * (# points in Circle) / (# points in Square).
Here is an attempt at approximating Pi with the above method:
#include <iostream> /* std::cout */
#include <iomanip> /* std::setprecision */
#include <random> /* std::uniform_int_distribution; std::mt19937 */
/* Check if point (x,y) is inside a circle with radius: r, at O(0,0). */
bool isInside (double x, double y, double r) { return (x*x + y*y) <= r*r; }
double approximatePi (double s, int sample_size)
{
std::mt19937 gen; /* Generate random number in [-s/2 : s/2]. */
std::uniform_int_distribution<double> dis(-s/2, s/2);
int count = 0; /* Number of points in the circle. */
for (int i = 0; i < sample_size; ++i)
{
double x = dis(gen);
double y = dis(gen);
if (isInside(x, y, s/2)) /* Radius of inscribed circle = side / 2. */
{
++count;
}
}
/* Pi = 4 * (# points in Circle) / (# points in Square). */
return (double) 4 * count / sample_size;
}
int main()
{
double side = 10.0; /* Square side. */
int sample_size = 10000; /* Number of tries. */
std::cout <<"Pi ~ "<< std::fixed << std::setprecision(6) << approximatePi(side, sample_size) << '\n';
}
Expected result: Pi ~ 3.141592
Actual result: Pi ~ 2.611200
Why am I not getting the expected result? What am I doing wrong?
The effect of std::uniform_int_distribution<double> is undefined behavior, because double is not an integer type.
Change it to std::uniform_real_distribution<double>.
What would be the best way of resampling a curve into even length segments using C++? What I have is a set of points that represents a 2d curve. In my example below I have a point struct with x and y components and a vector of points with test positions. Each pair of points represents a segment on the curve. Example resample curves are the images below. The red circles are the original positions the green circles are the target positions after the resample.
struct Point
{
float x, y;
};
std::vector<Point> Points;
int numPoints = 5;
float positions[] = {
0.0350462, -0.0589667,
0.0688311, 0.240896,
0.067369, 0.557199,
-0.024258, 0.715255,
0.0533231, 0.948694,
};
// Add points
int offset = 2;
for (int i =0; i < numPoints; i++)
{
offset = i * 2;
Point pt;
pt.x = positions[offset];
pt.y = positions[offset+1];
Points.push_back(pt);
}
See if this works for you. Resampled points are equidistant from each other on the linear interpolation of the source vector's points.
#include <iostream>
#include <iomanip>
#include <vector>
#include <cmath>
struct Point {
double x, y;
};
// Distance gives the Euclidean distance between two Points
double Distance(const Point& a, const Point& b) {
const double dx = b.x - a.x;
const double dy = b.y - a.y;
const double lsq = dx*dx + dy*dy;
return std::sqrt(lsq);
}
// LinearCurveLength calculates the total length of the linear
// interpolation through a vector of Points. It is the sum of
// the Euclidean distances between all consecutive points in
// the vector.
double LinearCurveLength(std::vector<Point> const &points) {
auto start = points.begin();
if(start == points.end()) return 0;
auto finish = start + 1;
double sum = 0;
while(finish != points.end()) {
sum += Distance(*start, *finish);
start = finish++;
}
return sum;
}
// Gives a vector of Points which are sampled as equally-spaced segments
// taken along the linear interpolation between points in the source.
// In general, consecutive points in the result will not be equidistant,
// because of a corner-cutting effect.
std::vector<Point> UniformLinearInterpolation(std::vector<Point> const &source, std::size_t target_count) {
std::vector<Point> result;
if(source.size() < 2 || target_count < 2) {
// degenerate source vector or target_count value
// for simplicity, this returns an empty result
// but special cases may be handled when appropriate for the application
return result;
}
// total_length is the total length along a linear interpolation
// of the source points.
const double total_length = LinearCurveLength(source);
// segment_length is the length between result points, taken as
// distance traveled between these points on a linear interpolation
// of the source points. The actual Euclidean distance between
// points in the result vector can vary, and is always less than
// or equal to segment_length.
const double segment_length = total_length / (target_count - 1);
// start and finish are the current source segment's endpoints
auto start = source.begin();
auto finish = start + 1;
// src_segment_offset is the distance along a linear interpolation
// of the source curve from its first point to the start of the current
// source segment.
double src_segment_offset = 0;
// src_segment_length is the length of a line connecting the current
// source segment's start and finish points.
double src_segment_length = Distance(*start, *finish);
// The first point in the result is the same as the first point
// in the source.
result.push_back(*start);
for(std::size_t i=1; i<target_count-1; ++i) {
// next_offset is the distance along a linear interpolation
// of the source curve from its beginning to the location
// of the i'th point in the result.
// segment_length is multiplied by i here because iteratively
// adding segment_length could accumulate error.
const double next_offset = segment_length * i;
// Check if next_offset lies inside the current source segment.
// If not, move to the next source segment and update the
// source segment offset and length variables.
while(src_segment_offset + src_segment_length < next_offset) {
src_segment_offset += src_segment_length;
start = finish++;
src_segment_length = Distance(*start, *finish);
}
// part_offset is the distance into the current source segment
// associated with the i'th point's offset.
const double part_offset = next_offset - src_segment_offset;
// part_ratio is part_offset's normalized distance into the
// source segment. Its value is between 0 and 1,
// where 0 locates the next point at "start" and 1
// locates it at "finish". In-between values represent a
// weighted location between these two extremes.
const double part_ratio = part_offset / src_segment_length;
// Use part_ratio to calculate the next point's components
// as weighted averages of components of the current
// source segment's points.
result.push_back({
start->x + part_ratio * (finish->x - start->x),
start->y + part_ratio * (finish->y - start->y)
});
}
// The first and last points of the result are exactly
// the same as the first and last points from the input,
// so the iterated calculation above skips calculating
// the last point in the result, which is instead copied
// directly from the source vector here.
result.push_back(source.back());
return result;
}
int main() {
std::vector<Point> points = {
{ 0.0350462, -0.0589667},
{ 0.0688311, 0.240896 },
{ 0.067369, 0.557199 },
{-0.024258, 0.715255 },
{ 0.0533231, 0.948694 }
};
std::cout << "Source Points:\n";
for(const auto& point : points) {
std::cout << std::setw(14) << point.x << " " << std::setw(14) << point.y << '\n';
}
std::cout << '\n';
auto interpolated = UniformLinearInterpolation(points, 7);
std::cout << "Interpolated Points:\n";
for(const auto& point : interpolated) {
std::cout << std::setw(14) << point.x << " " << std::setw(14) << point.y << '\n';
}
std::cout << '\n';
std::cout << "Source linear interpolated length: " << LinearCurveLength(points) << '\n';
std::cout << "Interpolation's linear interpolated length: " << LinearCurveLength(interpolated) << '\n';
}
For green points equidistant along the polyline:
The first run: walk through point list, calculate length of every segment and cumulative length up to current point. Pseudocode:
cumlen[0] = 0;
for (int i=1; i < numPoints; i++) {
len = Sqrt((Point[i].x - Point[i-1].x)^2 + (Point[i].y - Point [i-1].y)^2)
cumlen[i] = cumlen[i-1] + len;
}
Now find length of every new piece
plen = cumlen[numpoints-1] / numpieces;
Now the second run - walk through point list and insert new points in appropriate segments.
i = 0;
for (ip=0; ip<numpieces; ip++) {
curr = plen * ip;
while cumlen[i+1] < curr
i++;
P[ip].x = Points[i].x + (curr - cumlen[i]) * (Points[i+1].x - Points[i].x) /
(cumlen[i+1] - cumlen[i]);
..the same for y
}
Examples of real output for numpieces > numPoints and vice versa
The probability distribution of interest is
double x; // range: -pi/2.0 to +pi/2.0
double y = std::pow(std::cos(x), 2.0);
This function can be integrated analytically, however it cannot be inverted. Therefore the usual trick of mapping a uniform distribution to the required probability distribution cannot be performed.
Is there another method which can be used to generate a random variable cos^2(theta) distribution?
It may be possible to find the inverse function numerically, however I do not know of an efficient (memory and computationally) method of doing this.
From Inverse transform sampling: you can generate sample numbers at random from any probability distribution, given its cdf.
Say you want cos2x distribution, from -pi/2 to pi/2. Since integral of cos2x from -pi/2 to pi/2 is pi/2, you need to scale down so that the integral is 1. Thus, the pdf P(x) = (2/pi)cos2x
Next step is calculate cdf from given pdf, which is the pdf's integral. You can use any numerical method to find integral of P(x). Or you can go to Wolfram Alpha and get the answer: cdf is F(x) = (2/pi)(0.5x + 0.25sin2x) + 0.5
Next you need to calcluate F-1(x). Since F(x) is a monotonically increasing function, you can use bisection method (binary search) to find F-1(x) easily. Wolfram Alpha doesn't have this F-1(x) formula though.
Then generate a uniform real number u from 0 to 1. Your custom distribution is F-1(u).
#include <iostream>
#include <cmath>
#include <random>
#include <boost/random/random_device.hpp>
#include <vector>
#include <iomanip>
const double pi = 3.14159265358979323846;
const double LOW = -pi/2;
const double HIGH = pi/2;
double pdf(double x)
{
return cos(x) * cos(x);
}
double cdf(double x) //integral of pdf
{
return (2/pi)*(x/2 + sin(2*x)/4) + 0.5; //from Wolfram Alpha
}
double inverse_cdf(double u)
{ //bisection, not 100% accurate
double low = LOW;
double high = HIGH;
double epsilon = 1e-10; //any small number, e.g. 1e-15
while (high - low > epsilon)
{
double mid = (low + high) / 2;
if (cdf(mid) == u) return mid;
if (cdf(mid) < u) low = mid; else high = mid;
}
return (low + high) / 2;
}
double custom_distribution(std::mt19937& rng)
{
double u = std::uniform_real_distribution<double>(0,1)(rng);
return inverse_cdf(u);
}
int main()
{
std::mt19937 rng{boost::random::random_device{}()};
std::vector<double> xCount(15);
int nSamples = 10000;
double gap = (HIGH-LOW) / xCount.size();
while (nSamples--) xCount[(int)( (custom_distribution(rng) - LOW) / gap )]++;
for (int i = 0; i < xCount.size(); ++i)
{
std::cout << std::setw(2) << i << ":" << xCount[i] << "\t";
for (int bar = xCount[i]/15; bar--; std::cout << '*');
std::cout << "\n";
}
}
sample output:
0:17 *
1:135 *********
2:305 ********************
3:604 ****************************************
4:859 *********************************************************
5:1106 *************************************************************************
6:1256 ***********************************************************************************
7:1353 ******************************************************************************************
8:1271 ************************************************************************************
9:1102 *************************************************************************
10:876 **********************************************************
11:614 ****************************************
12:334 **********************
13:143 *********
14:25 *
I would like to simulate a point mass within a closed box. There is no friction and the point mass obeys the impact law. So there are only elastic collisions with the walls of the box. The output of the program is the time, position (rx,ry ,rz) and velocity (vx,vy,vz). I plot the trajectory by using GNUplot.
The problem I have now is, that the point mass gets energy from somewhere. So their jumps get each time more intense.
Is someone able to check my code?
/* Start of the code */
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
struct pointmass
{
double m; // mass
double r[3]; // coordinates
double v[3]; // velocity
};
// Grav.constant
const double G[3] = {0, -9.81, 0};
int main()
{
int Time = 0; // Duration
double Dt = 0; // Time steps
pointmass p0;
cerr << "Duration: ";
cin >> Time;
cerr << "Time steps: ";
cin >> Dt;
cerr << "Velocity of the point mass (vx,vy,vz)? ";
cin >> p0.v[0];
cin >> p0.v[1];
cin >> p0.v[2];
cerr << "Initial position of the point mass (x,y,z)? ";
cin >> p0.r[0];
cin >> p0.r[1];
cin >> p0.r[2];
for (double i = 0; i<Time; i+=Dt)
{
cout << i << setw(10);
for (int j = 0; j<=2; j++)
{
////////////position and velocity///////////
p0.r[j] = p0.r[j] + p0.v[j]*i + 0.5*G[j]*i*i;
p0.v[j] = p0.v[j] + G[j]*i;
///////////////////reflection/////////////////
if(p0.r[j] >= 250)
{
p0.r[j] = 500 - p0.r[j];
p0.v[j] = -p0.v[j];
}
else if(p0.r[j] <= 0)
{
p0.r[j] = -p0.r[j];
p0.v[j] = -p0.v[j];
}
//////////////////////////////////////////////
}
/////////////////////Output//////////////////
for(int j = 0; j<=2; j++)
{
cout << p0.r[j] << setw(10);
}
for(int j = 0; j<=2; j++)
{
cout << p0.v[j] << setw(10);
}
///////////////////////////////////////////////
cout << endl;
}
}
F = ma
a = F / m
a dt = F / m dt
a dt is acceleration over a fixed time - the change in velocity for that frame.
You are setting it to F / m i
it is that i which is wrong, as comments have suggested. It needs to be the duration of a frame, not the duration of the entire simulation so far.
I am a little concerned about the time loop along with other commenters - make sure that it represents an increment of time, not a growing duration.
Still, I think the main problem is you are changing the sign of all three components of velocity
on reflection.
That's not consistent with the laws of physics -conservation of linear momentum and energy - at the boundaries.
To see this, consider the case if your particle is moving in just the x-y plane (velocity in z is zero) and about to hit the wall at x= L.
The collision looks like this:
The force exerted on the point mass by the wall acts perpendicular to the wall. So there is no change in the momentum component of the particle parallel to the wall.
Applying conservation of linear momentum and kinetic energy, and assuming a perfectly elastic collision, you will find that
The component of velocity perpendicular to the wall DOES change sign
The component of velocity parallel to the wall DOES NOT change sign
In three dimensions, to have an accurate simulation, you have to work out the momentum components parallel and perpendicular to the wall on collision and code the resulting velocity changes.
In other words, this code:
///////////////////reflection/////////////////
if(p0.r[j] >= 250)
{
p0.r[j] = 500 - p0.r[j];
p0.v[j] = -p0.v[j];
}
else if(p0.r[j] <= 0)
{
p0.r[j] = -p0.r[j];
p0.v[j] = -p0.v[j];
}
//////////////////////////////////////////////
does not model the physics of reflection correctly. To fix it here is an outline of what to do:
Take the reflection checks out of the loop over x,y,z coordinates (but still within the time loop)
The collision condition for all six walls needs to be checked,
according to the direction of the normal vector to the wall.
For example for the right wall of the cube defined by X=250, 0<=Y<250, 0<=Z<250, the normal vector is in the negative X direction. For the left wall defined by X=0, 0<=Y<250, 0<=Z<250, the normal vector is in the positive X direction.
So on reflection from those two walls, the X component of velocity changes sign because it is normal (perpendicular) to the wall, but the Y and Z components do NOT change sign because they are parallel to the wall.
Apply similar considerations at the top and bottom wall (constant Y), and front and back wall (constant Z), of the cube -left as exercise to work out the normals to those surfaces.
Finally you shouldn't change sign of the position vector components on reflection, just the velocity vector. Instead recompute the next value of the position vector given the new velocity.
OK, so there are a few issues. The others have pointed out the need to use Dt rather than i for the integration step.
However, you are correct in stating that there is an issue with the reflection and energy conservation. I've added an explicit track of that below.
Note that the component wise computation of the reflection is actually fine other than the energy issue.
The problem was that during a reflection the acceleration due to gravity changes. In the case of the particle hitting the floor, it was acquiring kinetic energy equal to that it would have had if it had kept falling, but the new position had higher potential energy. So the energy would increase by exactly twice the potential energy difference between the floor and the new position. A bounce off the roof would have the opposite effect.
As noted below, once strategy would be to compute the actual time of reflection. However, actually working directly with energy is much simpler as well as more robust. However, please note although the the simple energy version below ensures that the speed and position are consistent, it actually does not have the correct position. For most purposes that may not actually matter. If you really need the correct position, I think we need to solve for the bounce time.
/* Start of the code */
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
struct pointmass
{
double m; // mass
double r[3]; // coordinates
double v[3]; // velocity
};
// Grav.constant
const double G[3] = { 0, -9.81, 0 };
int main()
{
// I've just changed the initial values to speed up unit testing; your code worked fine here.
int Time = 50; // Duration
double Dt = 1; // Time steps
pointmass p0;
p0.v[0] = 23;
p0.v[1] = 40;
p0.v[2] = 15;
p0.r[0] = 100;
p0.r[1] = 200;
p0.r[2] = 67;
for (double i = 0; i<Time; i += Dt)
{
cout << setw(10) << i << setw(10);
double energy = 0;
for (int j = 0; j <= 2; j++)
{
double oldR = p0.r[j];
double oldV = p0.v[j];
////////////position and velocity///////////
p0.r[j] = p0.r[j] + p0.v[j] * Dt + 0.5*G[j] * Dt*Dt;
p0.v[j] = p0.v[j] + G[j] * Dt;
///////////////////reflection/////////////////
if (G[j] == 0)
{
if (p0.r[j] >= 250)
{
p0.r[j] = 500 - p0.r[j];
p0.v[j] = -p0.v[j];
}
else if (p0.r[j] <= 0)
{
p0.r[j] = -p0.r[j];
p0.v[j] = -p0.v[j];
}
}
else
{
// Need to capture the fact that the acceleration switches direction relative to velocity half way through the timestep.
// Two approaches, either
// Try to compute the time of the bounce and work out the detail.
// OR
// Use conservation of energy to get the right speed - much easier!
if (p0.r[j] >= 250)
{
double energy = 0.5*p0.v[j] * p0.v[j] - G[j] * p0.r[j];
p0.r[j] = 500 - p0.r[j];
p0.v[j] = -sqrt(2 * (energy + G[j] * p0.r[j]));
}
else if (p0.r[j] <= 0)
{
double energy = 0.5*p0.v[j] * p0.v[j] - G[j] * p0.r[j];
p0.r[j] = -p0.r[j];
p0.v[j] = sqrt(2*(energy + G[j] * p0.r[j]));
}
}
energy += 0.5*p0.v[j] * p0.v[j] - G[j] * p0.r[j];
}
/////////////////////Output//////////////////
cout << energy << setw(10);
for (int j = 0; j <= 2; j++)
{
cout << p0.r[j] << setw(10);
}
for (int j = 0; j <= 2; j++)
{
cout << p0.v[j] << setw(10);
}
///////////////////////////////////////////////
cout << endl;
}
}