I require a function to calculate the distance between a pair of WGS 84 positions to a high degree of accuracy and I was planning to use the geographic functions from boost geometry.
The boost geometry Design Rational states:
There is the Andoyer method, fast and precise, and there is the Vincenty method, slower and more precise..
However, when testing the boost::geometry::distance function with both the Andoyer and Vincenty strategies, I got the following results:
WGS 84 values (metres)
Semimajor axis: 6378137.000000
Flattening: 0.003353
Semiminor axis: 6356752.314245
Semimajor distance: 20037508.342789
Semiminor distance: 19970326.371123
Boost geometry near poles
Andoyer function:
Semimajor distance: 20037508.151445
Semiminor distance: 20003917.164970
Vincenty function:
Semimajor distance: **19970326.180419**
Semiminor distance: 20003931.266635
Boost geometry at poles
Andoyer function:
Semimajor distance: 0.000000
Semiminor distance: 0.000000
Vincenty function:
Semimajor distance: **19970326.371122**
Semiminor distance: 20003931.458623
The Vincenty distances along the Semimajor axis (i.e. around the Equator) are less than the distance around the Semiminor axis between the North and South Poles. That can't be correct.
The Semiminor and Andoyer distances look reasonable. Except when the points are on opposing side of the Earth, when the boost Andoyer function returns zero!
Is the problem in: the Vincenty algorithm, the boost geometry implementation of it, or my test code?
Test code:
/// boost geometry WGS84 distance issue
// Note: M_PI is not part of the C or C++ standards, _USE_MATH_DEFINES enables it
#define _USE_MATH_DEFINES
#include <boost/geometry.hpp>
#include <cmath>
#include <iostream>
#include <ios>
// WGS 84 parameters from: Eurocontrol WGS 84 Implementation Manual
// Version 2.4 Chapter 3, page 14
/// The Semimajor axis measured in metres.
/// This is the radius at the equator.
constexpr double a = 6378137.0;
/// Flattening, a ratio.
/// This is the flattening of the ellipse at the poles
constexpr double f = 1.0/298.257223563;
/// The Semiminor axis measured in metres.
/// This is the radius at the poles.
/// Note: this is derived from the Semimajor axis and the flattening.
/// See WGS 84 Implementation Manual equation B-2, page 69.
constexpr double b = a * (1.0 - f);
int main(int /*argc*/, char ** /*argv*/)
{
std::cout.setf(std::ios::fixed);
std::cout << "WGS 84 values (metres)\n";
std::cout << "\tSemimajor axis:\t\t" << a << "\n";
std::cout << "\tFlattening:\t\t" << f << "\n";
std::cout << "\tSemiminor axis:\t\t" << b << "\n\n";
std::cout << "\tSemimajor distance:\t" << M_PI * a << "\n";
std::cout << "\tSemiminor distance:\t" << M_PI * b << "\n";
std::cout << std::endl;
// Min value for delta. 0.000000014 causes Andoyer to fail.
const double DELTA(0.000000015);
// For boost::geometry:
typedef boost::geometry::cs::geographic<boost::geometry::radian> Wgs84Coords;
typedef boost::geometry::model::point<double, 2, Wgs84Coords> GeographicPoint;
// Note boost points are Long & Lat NOT Lat & Long
GeographicPoint near_north_pole (0.0, M_PI_2 - DELTA);
GeographicPoint near_south_pole (0.0, -M_PI_2 + DELTA);
GeographicPoint near_equator_east ( M_PI_2 - DELTA, 0.0);
GeographicPoint near_equator_west (-M_PI_2 + DELTA, 0.0);
// Note: the default boost geometry spheroid is WGS84
// #include <boost/geometry/core/srs.hpp>
typedef boost::geometry::srs::spheroid<double> SpheroidType;
SpheroidType spheriod;
//#include <boost/geometry/strategies/geographic/distance_andoyer.hpp>
typedef boost::geometry::strategy::distance::andoyer<SpheroidType>
AndoyerStrategy;
AndoyerStrategy andoyer(spheriod);
std::cout << "Boost geometry near poles\n";
std::cout << "Andoyer function:\n";
double andoyer_major(boost::geometry::distance(near_equator_east, near_equator_west, andoyer));
std::cout << "\tSemimajor distance:\t" << andoyer_major << "\n";
double andoyer_minor(boost::geometry::distance(near_north_pole, near_south_pole, andoyer));
std::cout << "\tSemiminor distance:\t" << andoyer_minor << "\n";
//#include <boost/geometry/strategies/geographic/distance_vincenty.hpp>
typedef boost::geometry::strategy::distance::vincenty<SpheroidType>
VincentyStrategy;
VincentyStrategy vincenty(spheriod);
std::cout << "Vincenty function:\n";
double vincenty_major(boost::geometry::distance(near_equator_east, near_equator_west, vincenty));
std::cout << "\tSemimajor distance:\t" << vincenty_major << "\n";
double vincenty_minor(boost::geometry::distance(near_north_pole, near_south_pole, vincenty));
std::cout << "\tSemiminor distance:\t" << vincenty_minor << "\n\n";
// Note boost points are Long & Lat NOT Lat & Long
GeographicPoint north_pole (0.0, M_PI_2);
GeographicPoint south_pole (0.0, -M_PI_2);
GeographicPoint equator_east ( M_PI_2, 0.0);
GeographicPoint equator_west (-M_PI_2, 0.0);
std::cout << "Boost geometry at poles\n";
std::cout << "Andoyer function:\n";
andoyer_major = boost::geometry::distance(equator_east, equator_west, andoyer);
std::cout << "\tSemimajor distance:\t" << andoyer_major << "\n";
andoyer_minor = boost::geometry::distance(north_pole, south_pole, andoyer);
std::cout << "\tSemiminor distance:\t" << andoyer_minor << "\n";
std::cout << "Vincenty function:\n";
vincenty_major = boost::geometry::distance(equator_east, equator_west, vincenty);
std::cout << "\tSemimajor distance:\t" << vincenty_major << "\n";
vincenty_minor = boost::geometry::distance(north_pole, south_pole, vincenty);
std::cout << "\tSemiminor distance:\t" << vincenty_minor << "\n";
return 0;
}
I followed the advice of #jwd630 and checked out geographiclib.
Here are the results:
WGS 84 values (metres)
Semimajor distance: 20037508.342789
Semiminor distance: 19970326.371123
GeographicLib near antipodal
Semimajor distance: 20003931.458625
Semiminor distance: 20003931.455275
GeographicLib antipodal
Semimajor distance: 20003931.458625
Semiminor distance: 20003931.458625
GeographicLib verify
JFK to LHR distance: 5551759.400319
I.e. it provides the same distance as Vincenty for the Semiminor distance between the poles (to 5dp) and it calculates the same distance for antipodal points at the Equator.
This is correct, since the shortest distance between the antipodal points at the Equator is via one of the Poles, not around the equator as the default boost Andoyer algorithm calculates.
So #jwd630's answer above is right and of the three algorithms, geographiclib is the only one to calculate the correct distance over the whole WGS84 geoid.
Here is the test code:
/// GeographicLib WGS84 distance
// Note: M_PI is not part of the C or C++ standards, _USE_MATH_DEFINES enables it
#define _USE_MATH_DEFINES
#include <GeographicLib/Geodesic.hpp>
#include <cmath>
#include <iostream>
#include <ios>
// WGS 84 parameters from: Eurocontrol WGS 84 Implementation Manual
// Version 2.4 Chapter 3, page 14
/// The Semimajor axis measured in metres.
/// This is the radius at the equator.
constexpr double a = 6378137.0;
/// Flattening, a ratio.
/// This is the flattening of the ellipse at the poles
constexpr double f = 1.0/298.257223563;
/// The Semiminor axis measured in metres.
/// This is the radius at the poles.
/// Note: this is derived from the Semimajor axis and the flattening.
/// See WGS 84 Implementation Manual equation B-2, page 69.
constexpr double b = a * (1.0 - f);
int main(int /*argc*/, char ** /*argv*/)
{
const GeographicLib::Geodesic& geod(GeographicLib::Geodesic::WGS84());
std::cout.setf(std::ios::fixed);
std::cout << "WGS 84 values (metres)\n";
std::cout << "\tSemimajor axis:\t\t" << a << "\n";
std::cout << "\tFlattening:\t\t" << f << "\n";
std::cout << "\tSemiminor axis:\t\t" << b << "\n\n";
std::cout << "\tSemimajor distance:\t" << M_PI * a << "\n";
std::cout << "\tSemiminor distance:\t" << M_PI * b << "\n";
std::cout << std::endl;
// Min value for delta. 0.000000014 causes boost Andoyer to fail.
const double DELTA(0.000000015);
std::pair<double, double> near_equator_east (0.0, 90.0 - DELTA);
std::pair<double, double> near_equator_west (0.0, -90.0 + DELTA);
std::cout << "GeographicLib near antipodal\n";
double distance_metres(0.0);
geod.Inverse(near_equator_east.first, near_equator_east.second,
near_equator_west.first, near_equator_west.second, distance_metres);
std::cout << "\tSemimajor distance:\t" << distance_metres << "\n";
std::pair<double, double> near_north_pole (90.0 - DELTA, 0.0);
std::pair<double, double> near_south_pole (-90.0 + DELTA, 0.0);
geod.Inverse(near_north_pole.first, near_north_pole.second,
near_south_pole.first, near_south_pole.second, distance_metres);
std::cout << "\tSemiminor distance:\t" << distance_metres << "\n\n";
std::pair<double, double> equator_east (0.0, 90.0);
std::pair<double, double> equator_west (0.0, -90.0);
std::cout << "GeographicLib antipodal\n";
geod.Inverse(equator_east.first, equator_east.second,
equator_west.first, equator_west.second, distance_metres);
std::cout << "\tSemimajor distance:\t" << distance_metres << "\n";
std::pair<double, double> north_pole (90.0, 0.0);
std::pair<double, double> south_pole (-90.0, 0.0);
geod.Inverse(north_pole.first, north_pole.second,
south_pole.first, south_pole.second, distance_metres);
std::cout << "\tSemiminor distance:\t" << distance_metres << "\n\n";
std::pair<double, double> JFK (40.6, -73.8);
std::pair<double, double> LHR (51.6, -0.5);
std::cout << "GeographicLib verify distance\n";
geod.Inverse(JFK.first, JFK.second,
LHR.first, LHR.second, distance_metres);
std::cout << "\tJFK to LHR distance:\t" << distance_metres << std::endl;
return 0;
}
In his paper Algorithms for geodesics,
Charles F. F. Karney states that "Vincenty’s method fails to converge for nearly antipodal points".
Which may answer my original question, i.e. that the Vincenty algorithm isn't suitable for antipodal points.
Note: I've raised boost ticket #11817 describing the issue where
the Andoyer algorithm returns zero for antipodal points and sent a pull request to boost with a fix for it.
However, the only correct fix for the incorrect distances is to use the correct algorithm, namely: geographiclib
Many thanks to Charles F. F. Karney (#cffk) for politely pointing out my silly mistakes!
As an alternative check out Charles F. F. Karney's geographiclib. As the documentation says: "The emphasis is on returning accurate results with errors close to round-off (about 5–15 nanometers)."
Related
I am trying to use boost's convex_hull algorithm with longitude/latitude coordinates.
From here: https://archive.fosdem.org/2017/schedule/event/geo_boost_geography/attachments/slides/1748/export/events/attachments/geo_boost_geography/slides/1748/FOSDEM17_vissarion.pdf
I can see that we can calculate the distance between two points and even find the area using longitude/latitude coordinates (see page 19 and 22 of the PDF document).
Combining that with https://www.boost.org/doc/libs/1_75_0/libs/geometry/doc/html/geometry/reference/algorithms/convex_hull.html
I came up with this: https://wandbox.org/permlink/2AGPUtHPWrlGFMTf, but it does not compile, code here for convenience:
#include <iostream>
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/polygon.hpp>
#include <boost/geometry/geometries/adapted/boost_tuple.hpp>
namespace bg = boost::geometry;
int main()
{
typedef bg::model::point<double, 2, bg::cs::geographic<bg::degree>> point;
typedef boost::geometry::model::polygon<point> polygon;
polygon poly;
bg::read_wkt(" POLYGON ((4.346693 50.858306, 4.367945 50.852455, 4.366227 50.840809, 4.344961 50.833264, 4.338074 50.848677,4.346693 50.858306))",
poly );
polygon hull;
boost::geometry::convex_hull(poly, hull);
using boost::geometry::dsv;
std::cout << "polygon: " << dsv(poly) << std::endl << "hull: " << dsv(hull) << std::endl;
}
Any help is much appreciated.
Yeah, though you may be right that a strategy /can/ be made, that is not implemented.
A little side-by-side tester clearly shows that the strategy is not implemented for the geographical coordinate system:
template <typename cs> void test() {
using point = bg::model::point<double, 2, cs>;
using polygon = bg::model::polygon<point>;
polygon poly;
bg::read_wkt("POLYGON((4.346693 50.858306, 4.367945 50.852455, 4.366227 "
"50.840809, 4.344961 50.833264, 4.338074 50.848677,4.346693 "
"50.858306))",
poly);
std::cout << std::fixed;
std::cout << "Polygon: " << bg::dsv(poly) << std::endl;
std::cout << "Perimeter: " << bg::perimeter(poly) << std::endl;
std::cout << "Area: " << bg::area(poly) << std::endl;
using Strategy = typename bg::strategy_convex_hull<polygon, point>::type;
std::cout << "Strategy " << boost::core::demangle(typeid(Strategy).name()) << "\n";
if constexpr (not std::is_same_v<Strategy, bg::strategy::not_implemented>) {
polygon hull;
bg::convex_hull(poly, hull);
std::cout << "Hull: " << bg::dsv(hull) << std::endl;
}
}
It also demonstrates that some other strategies are implemented (e.g. distance).
See it Live On Coliru
int main() {
std::cout << "Cartesian:\n";
std::cout << "----------\n";
test<bg::cs::cartesian>();
std::cout << "\nGeographic:\n";
std::cout << "-----------\n";
test<bg::cs::geographic<bg::degree>>();
}
Simplifying the the typenames in the output:
Cartesian:
----------
Polygon: (((4.346693, 50.858306), (4.367945, 50.852455), (4.366227, 50.840809), (4.344961, 50.833264), (4.338074, 50.848677), (4.346693, 50.858306)))
Perimeter: 0.086184
Area: 0.000488
Strategy bg::strategy::convex_hull::graham_andrew<polygon, point>
Hull: (((4.338074, 50.848677), (4.346693, 50.858306), (4.367945, 50.852455), (4.366227, 50.840809), (4.344961, 50.833264), (4.338074, 50.848677)))
Geographic:
-----------
Polygon: (((4.346693, 50.858306), (4.367945, 50.852455), (4.366227, 50.840809), (4.344961, 50.833264), (4.338074, 50.848677), (4.346693, 50.858306)))
Perimeter: 7663.398262
Area: 3848183.734567
Strategy bg::strategy::not_implemented
A look at the documented strategies suggests that graham_andrew is in fact the only one available.
You should probably find out what tweaks are required to get things to work. it is technically possible to force convex_hull to use the Graham/Andrew strategy, but that seems ill-advised as the trait implies that the strategy is specifically deselected based on the coordinate system:
/*!
\brief Traits class binding a convex hull calculation strategy to a coordinate system
\ingroup convex_hull
\tparam Tag tag of coordinate system
\tparam Geometry the geometry type (hull operates internally per hull over geometry)
\tparam Point point-type of output points
*/
template
<
typename Geometry1,
typename Point,
typename CsTag = typename cs_tag<Point>::type
>
struct strategy_convex_hull
{
typedef strategy::not_implemented type;
};
Digging into the implementation of the strategy here's a hopeful hint:
// TODO: User-defiend CS-specific side strategy
typename strategy::side::services::default_strategy<cs_tag>::type side;
Perhaps we could be "done" with "just" specializing the Side Strategy for your coordinate systems? And more interestingly: a strategy::side::geographic exists. I'm out of my depth understanding the parameters (e.g. what the geodetic solution policy means?), but maybe yourself can take it from there?
I'm convinced that if you know what needs to be done, the helpful devs over at the mailing list will be very willing to guide the technical questions on how to fit it into the library.
I want to use the Efficient Ransac implementation of CGAL, but whenever I try to set my own parameters, the algorithm doesn't detect any shape anymore.
This work is related to the Polyfit implementation in CGAL. I want to fine tune the plane detection to see the influence it has on the algorithm. When I use the standard call to ransac.detect(), it works perfectly. However, when I want to set my own parameters it just doesn't find any plane, even if I set them manually to the default values.
Here is my code, strongly related to this example
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/IO/read_xyz_points.h>
#include <CGAL/IO/Writer_OFF.h>
#include <CGAL/property_map.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Shape_detection/Efficient_RANSAC.h>
#include <CGAL/Polygonal_surface_reconstruction.h>
#ifdef CGAL_USE_SCIP
#include <CGAL/SCIP_mixed_integer_program_traits.h>
typedef CGAL::SCIP_mixed_integer_program_traits<double> MIP_Solver;
#elif defined(CGAL_USE_GLPK)
#include <CGAL/GLPK_mixed_integer_program_traits.h>
typedef CGAL::GLPK_mixed_integer_program_traits<double> MIP_Solver;
#endif
#if defined(CGAL_USE_GLPK) || defined(CGAL_USE_SCIP)
#include <CGAL/Timer.h>
#include <fstream>
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
// Point with normal, and plane index
typedef boost::tuple<Point, Vector, int> PNI;
typedef std::vector<PNI> Point_vector;
typedef CGAL::Nth_of_tuple_property_map<0, PNI> Point_map;
typedef CGAL::Nth_of_tuple_property_map<1, PNI> Normal_map;
typedef CGAL::Nth_of_tuple_property_map<2, PNI> Plane_index_map;
typedef CGAL::Shape_detection::Efficient_RANSAC_traits<Kernel, Point_vector, Point_map, Normal_map> Traits;
typedef CGAL::Shape_detection::Efficient_RANSAC<Traits> Efficient_ransac;
typedef CGAL::Shape_detection::Plane<Traits> Plane;
typedef CGAL::Shape_detection::Point_to_shape_index_map<Traits> Point_to_shape_index_map;
typedef CGAL::Polygonal_surface_reconstruction<Kernel> Polygonal_surface_reconstruction;
typedef CGAL::Surface_mesh<Point> Surface_mesh;
int main(int argc, char ** argv)
{
Point_vector points;
// Loads point set from a file.
const std::string &input_file = argv[1];
//const std::string input_file(input);
std::ifstream input_stream(input_file.c_str());
if (input_stream.fail()) {
std::cerr << "failed open file \'" <<input_file << "\'" << std::endl;
return EXIT_FAILURE;
}
std::cout << "Loading point cloud: " << input_file << "...";
CGAL::Timer t;
t.start();
if (!input_stream ||
!CGAL::read_xyz_points(input_stream,
std::back_inserter(points),
CGAL::parameters::point_map(Point_map()).normal_map(Normal_map())))
{
std::cerr << "Error: cannot read file " << input_file << std::endl;
return EXIT_FAILURE;
}
else
std::cout << " Done. " << points.size() << " points. Time: " << t.time() << " sec." << std::endl;
// Shape detection
Efficient_ransac ransac;
ransac.set_input(points);
ransac.add_shape_factory<Plane>();
std::cout << "Extracting planes...";
t.reset();
// Set parameters for shape detection.
Efficient_ransac::Parameters parameters;
// Set probability to miss the largest primitive at each iteration.
parameters.probability = 0.05;
// Detect shapes with at least 500 points.
parameters.min_points = 100;
// Set maximum Euclidean distance between a point and a shape.
parameters.epsilon = 0.01;
// Set maximum Euclidean distance between points to be clustered.
parameters.cluster_epsilon = 0.01;
// Set maximum normal deviation.
// 0.9 < dot(surface_normal, point_normal);
parameters.normal_threshold = 0.9;
// Detect shapes.
ransac.detect(parameters);
//ransac.detect();
Efficient_ransac::Plane_range planes = ransac.planes();
std::size_t num_planes = planes.size();
std::cout << " Done. " << num_planes << " planes extracted. Time: " << t.time() << " sec." << std::endl;
// Stores the plane index of each point as the third element of the tuple.
Point_to_shape_index_map shape_index_map(points, planes);
for (std::size_t i = 0; i < points.size(); ++i) {
// Uses the get function from the property map that accesses the 3rd element of the tuple.
int plane_index = get(shape_index_map, i);
points[i].get<2>() = plane_index;
}
//////////////////////////////////////////////////////////////////////////
std::cout << "Generating candidate faces...";
t.reset();
Polygonal_surface_reconstruction algo(
points,
Point_map(),
Normal_map(),
Plane_index_map()
);
std::cout << " Done. Time: " << t.time() << " sec." << std::endl;
//////////////////////////////////////////////////////////////////////////
Surface_mesh model;
std::cout << "Reconstructing...";
t.reset();
if (!algo.reconstruct<MIP_Solver>(model)) {
std::cerr << " Failed: " << algo.error_message() << std::endl;
return EXIT_FAILURE;
}
const std::string& output_file(input_file+"_result.off");
std::ofstream output_stream(output_file.c_str());
if (output_stream && CGAL::write_off(output_stream, model))
std::cout << " Done. Saved to " << output_file << ". Time: " << t.time() << " sec." << std::endl;
else {
std::cerr << " Failed saving file." << std::endl;
return EXIT_FAILURE;
}
//////////////////////////////////////////////////////////////////////////
// Also stores the candidate faces as a surface mesh to a file
Surface_mesh candidate_faces;
algo.output_candidate_faces(candidate_faces);
const std::string& candidate_faces_file(input_file+"_candidate_faces.off");
std::ofstream candidate_stream(candidate_faces_file.c_str());
if (candidate_stream && CGAL::write_off(candidate_stream, candidate_faces))
std::cout << "Candidate faces saved to " << candidate_faces_file << "." << std::endl;
return EXIT_SUCCESS;
}
#else
int main(int, char**)
{
std::cerr << "This test requires either GLPK or SCIP.\n";
return EXIT_SUCCESS;
}
#endif // defined(CGAL_USE_GLPK) || defined(CGAL_USE_SCIP)
When launched, I have the following message:
Loading point cloud: Scene1/test.xyz... Done. 169064 points. Time: 0.428 sec.
Extracting planes... Done. 0 planes extracted. Time: 8.328 sec.
Generating candidate faces... Done. Time: 0.028 sec.
Reconstructing... Failed: at least 4 planes required to reconstruct a closed surface mesh (only 1 provided)
While I have this when launching the code the ransac detection function without parameters:
Loading point cloud: Scene1/test.xyz... Done. 169064 points. Time: 0.448 sec.
Extracting planes... Done. 18 planes extracted. Time: 3.088 sec.
Generating candidate faces... Done. Time: 94.536 sec.
Reconstructing... Done. Saved to Scene1/test.xyz_result.off. Time: 30.28 sec.
Can someone help me setting my own parameters for the ransac shape detection?
However, when I want to set my own parameters it just doesn't find any
plane, even if I set them manually to the default values.
Just to be sure: "setting them manually to the default values" is not what you are doing in the code you shared.
Default values are documented as:
1% of the total number of points for min_points, which should be around 1700 points in your case, not 100
1% of the bounding box diagonal for epsilon and cluster_epsilon. For that obviously I don't know if that is what you used (0.01) as I don't have access to your point set, but if you want to reproduce default values, you should use the CGAL::Bbox_3 object at some point
If you use these values, there's no reason why it should behave differently than with no parameters given (if it does not work, then please let me know because there may be a bug).
The Eigen Library supports creating a matrix with axis-angle arguments. How do you perform the opposite and determine the axis-angle from a matrix (I'm not interested in the Euler set, just a single axis-angle result).
Eigen supplies an AngleAxis constructor that takes a matrix as an argument. So, it is simply:
#include <iostream>
#include <Eigen/Core>
#include <Eigen/Geometry>
int main()
{
Eigen::Vector3d axis;
axis.setRandom();
std::cout << axis << "\n\n";
axis.normalize();
std::cout << axis << "\n\n";
Eigen::Matrix3d mat;
mat = Eigen::AngleAxisd(0.256, axis);
Eigen::AngleAxisd newAngleAxis(mat);
std::cout << newAngleAxis.angle() << "\n" << newAngleAxis.axis() << "\n\n";
return 0;
}
Which outputs:
-0.997497
0.127171
-0.613392
-0.846852
0.107965
-0.520755
0.256
-0.846852
0.107965
-0.520755
or something similar.
I am learning Intel's TBB library. When summing all values in a std::vector the result of tbb::parallel_reduce differs from std::accumulate in the case of more than 16.777.220 elements in the vector (errors experienced at 16.777.320 elements). Here is my minimum-working-example:
#include <iostream>
#include <vector>
#include <numeric>
#include <limits>
#include "tbb/tbb.h"
int main(int argc, const char * argv[]) {
int count = std::numeric_limits<int>::max() * 0.0079 - 187800; // - 187900 works
std::vector<float> heights(size);
std::fill(heights.begin(), heights.end(), 1.0f);
float ssum = std::accumulate(heights.begin(), heights.end(), 0);
float psum = tbb::parallel_reduce(tbb::blocked_range<std::vector<float>::iterator>(heights.begin(), heights.end()), 0,
[](tbb::blocked_range<std::vector<float>::iterator> const& range, float init) {
return std::accumulate(range.begin(), range.end(), init);
}, std::plus<float>()
);
std::cout << std::endl << " Heights serial sum: " << ssum << " parallel sum: " << psum;
return 0;
}
which outputs on my OSX 10.10.3 with XCode 6.3.1 and tbb stable 4.3-20141023 (poured from Brew):
Heights serial sum: 1.67772e+07 parallel sum: 1.67773e+07
Why is that? Should I report an error to TBB developers?
Additional testing, applying your answers:
correct value is: 1949700403
cause we add 1.0f to zero 1949700403 times
using (int) init values:
Runtime: 17.407 sec. Heights serial sum: 16777216.000, wrong
Runtime: 8.482 sec. Heights parallel sum: 131127368.000, wrong
using (float) init values:
Runtime: 12.594 sec. Heights serial sum: 16777216.000, wrong
Runtime: 5.044 sec. Heights parallel sum: 303073632.000, wrong
using (double) initial values:
Runtime: 13.671 sec. Heights serial sum: 1949700352.000, wrong
Runtime: 5.343 sec. Heights parallel sum: 263690016.000, wrong
using (double) initial values and tbb::parallel_deterministic_reduce:
Runtime: 13.463 sec. Heights serial sum: 1949700352.000, wrong
Runtime: 99.031 sec. Heights parallel sum: 1949700352.000, wrong >>> almost 10x slower !
Why do all reduce calls produce the wrong sum? Is (double) not sufficient?
Here is my testing code:
#include <iostream>
#include <vector>
#include <numeric>
#include <limits>
#include <sys/time.h>
#include <iomanip>
#include "tbb/tbb.h"
#include <cmath>
class StopWatch {
private:
double elapsedTime;
timeval startTime, endTime;
public:
StopWatch () : elapsedTime(0) {}
void startTimer() {
elapsedTime = 0;
gettimeofday(&startTime, 0);
}
void stopNprintTimer() {
gettimeofday(&endTime, 0);
elapsedTime = (endTime.tv_sec - startTime.tv_sec) * 1000.0; // compute sec to ms
elapsedTime += (endTime.tv_usec - startTime.tv_usec) / 1000.0; // compute us to ms and add
std::cout << " Runtime: " << std::right << std::setw(6) << elapsedTime / 1000 << " sec."; // show in sec
}
};
int main(int argc, const char * argv[]) {
StopWatch watch;
std::cout << std::fixed << std::setprecision(3) << "" << std::endl;
size_t count = std::numeric_limits<int>::max() * 0.9079;
std::vector<float> heights(count);
std::cout << " Vector size: " << count << std::endl;
std::fill(heights.begin(), heights.end(), 1.0f);
watch.startTimer();
float ssum = std::accumulate(heights.begin(), heights.end(), 0.0); // change type of initial value here
watch.stopNprintTimer();
std::cout << " Heights serial sum: " << std::right << std::setw(8) << ssum << std::endl;
watch.startTimer();
float psum = tbb::parallel_reduce(tbb::blocked_range<std::vector<float>::iterator>(heights.begin(), heights.end()), 0.0, // change type of initial value here
[](tbb::blocked_range<std::vector<float>::iterator> const& range, float init) {
return std::accumulate(range.begin(), range.end(), init);
}, std::plus<float>()
);
watch.stopNprintTimer();
std::cout << " Heights parallel sum: " << std::right << std::setw(8) << psum << std::endl;
return 0;
}
Answer to my last question: they all produce wrong results because they are not made for integer addition with large numbers. Switching to int solves that:
[...]
std::vector<int> heights(count);
std::cout << " Vector size: " << count << std::endl;
std::fill(heights.begin(), heights.end(), 1);
watch.startTimer();
int ssum = std::accumulate(heights.begin(), heights.end(), (int)0);
watch.stopNprintTimer();
std::cout << " Heights serial sum: " << std::right << std::setw(8) << ssum << std::endl;
watch.startTimer();
int psum = tbb::parallel_reduce(tbb::blocked_range<std::vector<int>::iterator>(heights.begin(), heights.end()), (int)0,
[](tbb::blocked_range<std::vector<int>::iterator> const& range, int init) {
return std::accumulate(range.begin(), range.end(), init);
}, std::plus<int>()
);
watch.stopNprintTimer();
std::cout << " Heights parallel sum: " << std::right << std::setw(8) << psum << std::endl;
[...]
results in:
Vector size: 1949700403
Runtime: 13.041 sec. Heights serial sum: 1949700403, correct
Runtime: 4.728 sec. Heights parallel sum: 1949700403, correct and almost 4x faster
Your call to std::accumulate is doing integer addition, then transforming the result to float at the end of the calculation. In order to accumulate over floating point numbers, the accumulator should be a float*.
float ssum = std::accumulate(heights.begin(), heights.end(), 0.0f);
^^^^
* Or any other type that can accumulate float correctly.
To other correct answers for the 'why?' part, I'd also add that TBB provides parallel_deterministic_reduce which guarantees reproducible results between two and more runs on the same data (but it still can differ with std::accumulate). See the blog describing the issue and the deterministic algorithm.
Thus regarding 'Should I report an error to TBB developers?' part, the answer is obviously no (unless you find something insufficient on the TBB side).
This may fix this particular problem for you:
Your call to std::accumulate is doing integer addition, then transforming the result to float at the end of the calculation.
BUT floating point addition is NOT an associative operation:
With accumulate: (...((s+a1)+a2)+...)+an
With parralel_reduce: any parenthesis permutation possible.
http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I tried to do this clipping example using Boost Geometry in the example here
But in the newest Boost library I can't find ...
boost/geometry/geometries/cartesian2d.hpp
Does anyone have any idea where I might find this file? Or is this tutorial too old, and the file now obsolete?
The tutorial is too old. Much has changed since that tutorial was written.
Anyhow, I've modified the code in your link above so that it compiles in Boost ver 1.53.0 ...
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/geometries/polygon.hpp>
#include <boost/geometry/io/wkt/wkt.hpp>
#include <boost/geometry/multi/geometries/multi_polygon.hpp>
using namespace boost::geometry;
typedef model::d2::point_xy<double> point;
typedef model::ring< point > ring;
typedef model::polygon< point > polygon;
typedef model::multi_polygon< polygon > polygons;
typedef model::box< point > box;
// Define a polygon and fill the outer ring.
// In most cases you will read it from a file or database
polygon poly;
{
read_wkt("POLYGON((2.0 1.3, 2.4 1.7, 2.8 1.8, 3.4 1.2, 3.7 1.6, \
3.4 2.0, 4.1 3.0, 5.3 2.6, 5.4 1.2, 4.9 0.8, 2.9 0.7, \
2.0 1.3))", poly);
}
// Polygons should be closed, and directed clockwise. If you're not sure if that is the case,
// call the correct algorithm
correct(poly);
// Polygons can be streamed as text
// (or more precisely: as DSV (delimiter separated values))
std::cout << dsv(poly) << std::endl;
// As with lines, bounding box of polygons can be calculated
box b;
envelope(poly, b);
std::cout << dsv(b) << std::endl;
// The area of the polygon can be calulated
std::cout << "area: " << area(poly) << std::endl;
// And the centroid, which is the center of gravity
point cent;
centroid(poly, cent);
std::cout << "centroid: " << dsv(cent) << std::endl;
// The number of points have to called per ring separately
std::cout << "number of points in outer ring: " << poly.outer().size() << std::endl;
// Polygons can have one or more inner rings, also called holes, donuts, islands, interior rings.
// Let's add one
{
poly.inners().resize(1);
ring& inner = poly.inners().back();
read_wkt("POLYGON((4.0 2.0, 4.2 1.4, 4.8 1.9, 4.4 2.2, 4.0 2.0))", inner);
}
correct(poly);
std::cout << "with inner ring:" << dsv(poly) << std::endl;
// The area of the polygon is changed of course
std::cout << "new area of polygon: " << area(poly) << std::endl;
centroid(poly, cent);
std::cout << "new centroid: " << dsv(cent) << std::endl;
// You can test whether points are within a polygon
std::cout << "point in polygon:"
<< " p1: " << (within(make<point>(3.0, 2.0), poly)?"true":"false")
<< " p2: " << (within(make<point>(3.7, 2.0), poly)?"true":"false")
<< " p3: " << (within(make<point>(4.4, 2.0), poly)?"true":"false")
<< std::endl;
// As with linestrings and points, you can derive from polygon to add, for example,
// fill color and stroke color. Or SRID (spatial reference ID). Or Z-value. Or a property map.
// We don't show this here.
// Clip the polygon using a bounding box
box cb(make<point>(1.5, 1.5), make<point>(4.5, 2.5));
polygons v;
intersection(cb, poly, v);
std::cout << "Clipped output polygons" << std::endl;
for (polygons::const_iterator it = v.begin(); it != v.end(); ++it)
{
std::cout << dsv(*it) << std::endl;
}
union_(cb, poly, v);
polygon hull;
convex_hull(poly, hull);
std::cout << "Convex hull:" << dsv(hull) << std::endl;
// If you really want:
// You don't have to use a vector, you can define a polygon with a deque
// You can specify the container for the points and for the inner rings independently
typedef model::polygon<point, true, true, std::vector, std::deque> polygon_deq;
polygon_deq poly2;
polygon_deq::ring_type& r = poly2.outer();
append(r, make<point>(2.8, 1.9));
append(r, make<point>(2.9, 2.4));
append(r, make<point>(3.3, 2.2));
append(r, make<point>(3.2, 1.8));
append(r, make<point>(2.8, 1.9));
std::cout << dsv(poly2) << std::endl;