GCC Code Coverage Report


Directory: ./
File: lib/exploragram/optimal_transport/optimal_transport_on_surface.cpp
Date: 2026-09-07 02:28:19
Exec Total Coverage
Lines: 0 178 0.0%
Functions: 0 20 0.0%
Branches: 0 168 0.0%

Line Branch Exec Source
1 /*
2 * Copyright (c) 2000-2022 Inria
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 * * Neither the name of the ALICE Project-Team nor the names of its
14 * contributors may be used to endorse or promote products derived from this
15 * software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 *
29 * Contact: Bruno Levy
30 *
31 * https://www.inria.fr/fr/bruno-levy
32 *
33 * Inria,
34 * Domaine de Voluceau,
35 * 78150 Le Chesnay - Rocquencourt
36 * FRANCE
37 *
38 */
39
40 #include <exploragram/optimal_transport/optimal_transport_on_surface.h>
41 #include <geogram/voronoi/generic_RVD_vertex.h>
42 #include <geogram/voronoi/generic_RVD_polygon.h>
43 #include <geogram/voronoi/RVD_callback.h>
44 #include <geogram/basic/geometry.h>
45 #include <geogram/basic/geometry_nd.h>
46 #include <geogram/basic/stopwatch.h>
47
48 #include <geogram/bibliography/bibliography.h>
49
50 namespace {
51 using namespace GEO;
52
53 /**********************************************************************/
54
55 // For more details on the algorithm / structure of the objective function,
56 //see also implementation notes at the beginning of optimal_transport_3d.cpp.
57
58 /**
59 * \brief Computes the contribution of a polygon
60 * to the objective function minimized by a semi-discrete
61 * optimal transport map in 2D.
62 */
63 class SurfaceOTMPolygonCallback :
64 public OptimalTransportMap::Callback,
65 public RVDPolygonCallback {
66 public:
67
68 /**
69 * \brief OTMPolygonCallback constructor.
70 * \param[in] OTM a pointer to the OptimalTransportMapOnSurface
71 */
72 SurfaceOTMPolygonCallback(OptimalTransportMapOnSurface* OTM) :
73 OptimalTransportMap::Callback(OTM) {
74 }
75
76 /**
77 * \copydoc RVDPolygonCallback::operator()
78 */
79 void operator() (
80 index_t v,
81 index_t t,
82 const GEOGen::Polygon& P
83 ) const override {
84 // v can be an air particle.
85 if(v >= n_) {
86 return;
87 }
88
89 if(P.nb_vertices() == 0) {
90 return;
91 }
92
93 double m, mgx, mgy, mgz;
94 compute_m_and_mg(P, m, mgx, mgy, mgz);
95
96 if(spinlocks_ != nullptr) {
97 spinlocks_->acquire_spinlock(v);
98 }
99
100 // +m because we maximize F <=> minimize -F
101 g_[v] += m;
102
103 if(Newton_step_) {
104 // ... but here -m because Newton step =
105 // solve H p = -g (minus g in the RHS).
106 OTM_->add_i_right_hand_side(v,-m);
107 }
108
109 if(mg_ != nullptr) {
110 mg_[3*v] += mgx;
111 mg_[3*v+1] += mgy;
112 mg_[3*v+2] += mgz;
113 }
114
115 if(spinlocks_ != nullptr) {
116 spinlocks_->release_spinlock(v);
117 }
118
119 if(Newton_step_) {
120 // Spinlocks are managed internally by update_Hessian().
121 update_Hessian(P, v, t);
122 }
123
124
125 if(eval_F_) {
126 Thread* thread = Thread::current();
127 index_t current_thread_id = (thread==nullptr) ? 0 : thread->id();
128 double F = weighted_ ? eval_F_weighted(P, v) : eval_F(P, v);
129 const_cast<SurfaceOTMPolygonCallback*>(this)->
130 funcval_[current_thread_id] += F;
131 }
132 }
133
134 protected:
135
136 /**
137 * \brief Computes the mass and mass times centroid of the
138 * current intersection polygon.
139 * \details Weights are taken into account if present.
140 * \param[in] P a const reference to the current intersection polygon.
141 * \param[out] m , mgx , mgy , mgz the mass and the mass times the
142 * centroid of the ConvexCell. mgx, mgy and mgy are not computed
143 * if mg_ is nullptr.
144 */
145 void compute_m_and_mg(
146 const GEOGen::Polygon& P,
147 double& m, double& mgx, double& mgy, double& mgz
148 ) const {
149 m = 0.0;
150 mgx = 0.0;
151 mgy = 0.0;
152 mgz = 0.0;
153 const GEOGen::Vertex& V0 = P.vertex(0);
154 const double* p0 = V0.point();
155 for(index_t i=1; i+1<P.nb_vertices(); ++i) {
156 const GEOGen::Vertex& V1 = P.vertex(i);
157 const double* p1 = V1.point();
158 const GEOGen::Vertex& V2 = P.vertex(i+1);
159 const double* p2 = V2.point();
160 double cur_m = triangle_mass(V0,V1,V2);
161 m += cur_m;
162 if(mg_ != nullptr) {
163 if(weighted_) {
164 double w0 = V0.weight();
165 double w1 = V1.weight();
166 double w2 = V2.weight();
167 double s = cur_m/(w0+w1+w2);
168 mgx += s * (w0*p0[0] + w1*p1[0] + w2*p2[0]);
169 mgy += s * (w0*p0[1] + w1*p1[1] + w2*p2[1]);
170 mgz += s * (w0*p0[2] + w1*p1[2] + w2*p2[2]);
171 } else {
172 mgx += cur_m * (p0[0] + p1[0] + p2[0]) / 3.0;
173 mgy += cur_m * (p0[1] + p1[1] + p2[1]) / 3.0;
174 mgz += cur_m * (p0[2] + p1[2] + p2[2]) / 3.0;
175 }
176 }
177 }
178 }
179
180 /**
181 * \brief Gets the unit normal vector to a facet.
182 */
183 vec3 facet_normal(index_t t) const {
184 Mesh& M = OTM_->mesh();
185 index_t i = M.facets.vertex(t,0);
186 index_t j = M.facets.vertex(t,1);
187 index_t k = M.facets.vertex(t,2);
188 const double* p0 = M.vertices.point_ptr(i);
189 const double* p1 = M.vertices.point_ptr(j);
190 const double* p2 = M.vertices.point_ptr(k);
191 double x1 = p1[0] - p0[0];
192 double y1 = p1[1] - p0[1];
193 double z1 = p1[2] - p0[2];
194 double x2 = p2[0] - p0[0];
195 double y2 = p2[1] - p0[1];
196 double z2 = p2[2] - p0[2];
197 double x = y1*z2 - z1*y2;
198 double y = z1*x2 - x1*z2;
199 double z = x1*y2 - y1*x2;
200 return vec3(x,y,z);
201 }
202
203 /**
204 * \brief Updates the Hessian according to the current intersection
205 * polygon.
206 * \param[in] P a const reference to the current intersection polygon.
207 * \param[in] i the current seed
208 * \param[in] t the current mesh facet
209 */
210 void update_Hessian(
211 const GEOGen::Polygon& P, index_t i, index_t t
212 ) const {
213
214 vec3 N = normalize(facet_normal(t));
215
216 // The coefficient of the Hessian associated to a pair of
217 // adjacent cells Lag(i),Lag(j) is :
218 // - mass(Lag(i) /\ Lag(j)) / (2*distance(pi,pj))
219
220 const double* pi = OTM_->point_ptr(i);
221
222 for(index_t k1=0; k1<P.nb_vertices(); ++k1) {
223 index_t k2 = k1+1;
224 if(k2 == P.nb_vertices()) {
225 k2 = 0;
226 }
227 // Note:
228 // It is P.vertex(k2).adjacent_seed(),
229 // not P.vertex(k1).adjacent_seed() !!!
230 index_t j = index_t(P.vertex(k2).adjacent_seed());
231 if(j != index_t(-1)) {
232 const double* pj = OTM_->point_ptr(j);
233
234 vec3 pij(pj[0] - pi[0],pj[1] - pi[1],pj[2] - pi[2]);
235 // Remove from pij the part that is normal
236 // to the current polygon.
237 pij -= dot(pij,N)*N;
238 double lij = length(pij);
239
240 double hij =
241 edge_mass(P.vertex(k1), P.vertex(k2)) / (2.0 * lij);
242
243 // -hij because we maximize F <=> minimize -F
244 if(hij != 0.0) {
245 if(spinlocks_ != nullptr) {
246 spinlocks_->acquire_spinlock(i);
247 }
248 // Diagonal is positive, extra-diagonal
249 // coefficients are negative,
250 // this is a convex function.
251 if(j < n_) {
252 OTM_->add_ij_coefficient(i, j, -hij);
253 }
254 OTM_->add_ij_coefficient(i, i, hij);
255 if(spinlocks_ != nullptr) {
256 spinlocks_->release_spinlock(i);
257 }
258 }
259 }
260 }
261 }
262
263 /**
264 * \brief Computes the contribution of the current polygon
265 * to the objective function, in the uniform (non-weighted)
266 * case.
267 * \param[in] P a const reference to the current polygon.
268 * \param[in] i the current seed
269 */
270 double eval_F(const GEOGen::Polygon& P, index_t i) const {
271 geo_debug_assert(!weighted_);
272 geo_argused(P);
273 geo_argused(i);
274 // Not implemented yet.
275 geo_assert_not_reached;
276 }
277
278 /**
279 * \brief Computes the contribution of the current polygon
280 * to the objective function, in the weighted case.
281 * \param[in] P a const reference to the current polygon.
282 * \param[in] i the current seed
283 */
284 double eval_F_weighted(const GEOGen::Polygon& P, index_t i) const {
285 geo_argused(P);
286 geo_argused(i);
287 // Not implemented yet.
288 geo_assert_not_reached;
289 }
290
291 /**
292 * \brief Computes the mass of a triangle.
293 * \details Weights are taken into account in weighted_ mode.
294 * \param[in] V0 , V1 , V2 the three vertices of the triangle,
295 * given as RVD vertices.
296 * \return the area of the triangle in 3D times the average value of the
297 * three weights.
298 */
299 double triangle_mass(
300 const GEOGen::Vertex& V0,
301 const GEOGen::Vertex& V1,
302 const GEOGen::Vertex& V2
303 ) const {
304 double m =
305 Geom::triangle_area_3d(V0.point(), V1.point(), V2.point());
306 if(weighted_) {
307 m *= ((V0.weight() + V1.weight() + V2.weight())/3.0);
308 }
309 return m;
310 }
311
312 /**
313 * \brief Computes the mass of an edge.
314 * \details Weights are taken into account in weighted_ mode.
315 * \param[in] V0 , V1 the two vertices of the edge,
316 * given as RVD vertices.
317 * \return the length of the edge in 3D times the average value of the
318 * two weights.
319 */
320 double edge_mass(
321 const GEOGen::Vertex& V0,
322 const GEOGen::Vertex& V1
323 ) const {
324 double m = Geom::distance(V0.point(), V1.point(), 3);
325 if(weighted_) {
326 m *= ((V0.weight() + V1.weight())/2.0);
327 }
328 return m;
329 }
330 };
331
332 /**********************************************************************/
333
334 /**
335 * \brief A RVDPolygonCallback that stores the Restricted Voronoi
336 * Diagram in a Mesh.
337 */
338 class ComputeRVDPolygonCallback : public RVDPolygonCallback {
339 public:
340 ComputeRVDPolygonCallback(OptimalTransportMap* OTM, Mesh* target) :
341 OTM_(OTM), target_(target) {
342 target_->clear();
343 target_->vertices.set_dimension(3);
344 chart_.bind(target_->facets.attributes(), "chart");
345 }
346
347 ~ComputeRVDPolygonCallback() override {
348 chart_.unbind();
349 }
350
351 void operator() (
352 index_t v,
353 index_t t,
354 const GEOGen::Polygon& P
355 ) const override {
356 geo_argused(v);
357 geo_argused(t);
358
359 if(P.nb_vertices() == 0) {
360 return;
361 }
362
363 index_t voffset = target_->vertices.nb();
364 FOR(i,P.nb_vertices()) {
365 const double* p = P.vertex(i).point();
366 if(OTM_->dimension() == 2) {
367 target_->vertices.create_vertex(
368 vec3(p[0], p[1], 0.0).data()
369 );
370 } else {
371 target_->vertices.create_vertex(
372 vec3(p[0], p[1], p[2]).data()
373 );
374 }
375 }
376 index_t f = target_->facets.create_polygon(P.nb_vertices());
377 FOR(i,P.nb_vertices()) {
378 target_->facets.set_vertex(f,i,voffset+i);
379 }
380 const_cast<Attribute<index_t>&>(chart_)[f] = v;
381 }
382
383 private:
384 OptimalTransportMap* OTM_;
385 Mesh* target_;
386 Attribute<index_t> chart_;
387 };
388
389
390
391 /**
392 * \brief Gets the Delaunay implementation that best fits
393 * user desire.
394 * \param[in] user_delaunay the desired implementation.
395 * \return the available implementation nearest to user desire.
396 */
397 std::string default_delaunay(const std::string& user_delaunay) {
398 std::string result = "BPOW";
399 if(user_delaunay != "default") {
400 result = user_delaunay;
401 }
402 #ifndef GEOGRAM_WITH_PDEL
403 if(result == "PDEL") {
404 result = "BPOW";
405 }
406 #endif
407 return result;
408 }
409
410
411 }
412
413 /**********************************************************************/
414
415 namespace GEO {
416
417 OptimalTransportMapOnSurface::OptimalTransportMapOnSurface(
418 Mesh* mesh, const std::string& delaunay, bool BRIO
419 ) :
420 OptimalTransportMap(
421 3,
422 mesh,
423 default_delaunay(delaunay),
424 BRIO
425 ) {
426 callback_ = new SurfaceOTMPolygonCallback(this);
427 total_mass_ = total_mesh_mass();
428 geo_cite("DBLP:journals/corr/MerigotMT17");
429 }
430
431 OptimalTransportMapOnSurface::~OptimalTransportMapOnSurface() {
432 }
433
434 void OptimalTransportMapOnSurface::get_RVD(Mesh& RVD_mesh) {
435 ComputeRVDPolygonCallback callback(this, &RVD_mesh);
436 RVD()->for_each_polygon(callback, false, false, false);
437 /*
438 // NOTE: Does not work, TODO: determine why
439 Attribute<index_t> tet_region(RVD_mesh.cells.attributes(),"region");
440 RVD()->compute_RVD(
441 RVD_mesh,
442 0, // dim (0 means use default)
443 false, // borders_only
444 show_RVD_seed_ // integration_simplices
445 );
446 */
447 }
448
449 void OptimalTransportMapOnSurface::compute_Laguerre_centroids(
450 double* centroids
451 ) {
452 vector<double> g(nb_points(), 0.0);
453 Memory::clear(centroids, nb_points()*sizeof(double)*3);
454
455 callback_->set_Laguerre_centroids(centroids);
456 callback_->set_g(g.data());
457 {
458 Stopwatch* W = nullptr;
459 if(newton_ && verbose_) {
460 W = new Stopwatch("RVD");
461 Logger::out("OTM") << "In RVD (centroids)..." << std::endl;
462 }
463 RVD_->for_each_polygon(
464 *dynamic_cast<RVDPolygonCallback*>(callback_), false, false, true
465 );
466 if(newton_ && verbose_) {
467 delete W;
468 }
469 }
470
471 callback_->set_Laguerre_centroids(nullptr);
472
473 for(index_t v=0; v<nb_points(); ++v) {
474 centroids[3*v ] /= g[v];
475 centroids[3*v+1] /= g[v];
476 centroids[3*v+2] /= g[v];
477 }
478 }
479
480 double OptimalTransportMapOnSurface::total_mesh_mass() const {
481 double result = 0.0;
482
483 // This is terribly confusing, the parameters for
484 // a power diagram are called "weights", and the
485 // standard attribute name for vertices density is
486 // also called "weight" (and is unrelated).
487 // In this program, what is called weight corresponds
488 // to the parameters of the power diagram (except the
489 // name of the attribute), and everything that corresponds
490 // to mass/density is called mass.
491 Attribute<double> vertex_mass;
492 vertex_mass.bind_if_is_defined(
493 mesh_->vertices.attributes(), "weight"
494 );
495
496 for(index_t t: mesh_->facets) {
497 double tri_mass = GEO::Geom::triangle_area(
498 vec3(mesh_->vertices.point_ptr(mesh_->facets.vertex(t, 0))),
499 vec3(mesh_->vertices.point_ptr(mesh_->facets.vertex(t, 1))),
500 vec3(mesh_->vertices.point_ptr(mesh_->facets.vertex(t, 2)))
501 );
502 if(vertex_mass.is_bound()) {
503 tri_mass *= (
504 vertex_mass[mesh_->facets.vertex(t, 0)] +
505 vertex_mass[mesh_->facets.vertex(t, 1)] +
506 vertex_mass[mesh_->facets.vertex(t, 2)]
507 ) / 3.0;
508 }
509 result += tri_mass;
510 }
511 return result;
512 }
513
514 void OptimalTransportMapOnSurface::call_callback_on_RVD() {
515 RVD_->for_each_polygon(
516 *dynamic_cast<RVDPolygonCallback*>(callback_),
517 false, // symbolic
518 false, // connected components priority
519 !clip_by_balls_ // parallel
520 );
521 // clip_by_balls deactivates parallel mode, because it needs to access
522 // the PointAllocator of the current thread, and we do not have any
523 // access (for now).
524 }
525
526 /**********************************************************************/
527
528 void compute_Laguerre_centroids_on_surface(
529 Mesh* omega,
530 index_t nb_points,
531 const double* points,
532 double* centroids,
533 Mesh* RVD,
534 bool verbose
535 ) {
536
537 omega->vertices.set_dimension(4);
538
539 // false = no BRIO
540 // (OTM does not use multilevel and lets Delaunay
541 // reorder the vertices)
542 OptimalTransportMapOnSurface OTM(
543 omega,
544 "BPOW", // "PDEL" will not be much faster because
545 // we are on a surface (lots of threads interactions).
546 false
547 );
548
549 static bool initialized = false;
550 static bool has_cholmod = false;
551 if(!initialized) {
552 initialized = true;
553 has_cholmod = (nlInitExtension("CHOLMOD") == NL_TRUE);
554 }
555 if(has_cholmod) {
556 OTM.set_regularization(1e-3);
557 OTM.set_linear_solver(OT_CHOLMOD);
558 }
559
560 OTM.set_Newton(true);
561 OTM.set_points(nb_points, points);
562 OTM.set_epsilon(0.01);
563 OTM.set_Laguerre_centroids(centroids);
564 OTM.set_verbose(verbose);
565 OTM.optimize(1000);
566
567 if(RVD != nullptr) {
568 OTM.get_RVD(*RVD);
569 }
570
571 omega->vertices.set_dimension(3);
572 }
573
574 /**********************************************************************/
575
576 }
577