Graphviz 13.0.0~dev.20241220.2304
Loading...
Searching...
No Matches
quad_prog_vpsc.c
Go to the documentation of this file.
1
17/**********************************************************
18 *
19 * Solve a quadratic function f(X) = X' e->A X + b X
20 * subject to a set of separation constraints e->cs
21 *
22 * Tim Dwyer, 2006
23 **********************************************************/
24
25#include <neatogen/digcola.h>
26#include <stdbool.h>
27#include <util/alloc.h>
28#ifdef IPSEPCOLA
29#include <math.h>
30#include <stdlib.h>
31#include <time.h>
32#include <stdio.h>
33#include <float.h>
34#include <assert.h>
35#include <neatogen/matrix_ops.h>
36#include <neatogen/kkutils.h>
37#include <vpsc/csolve_VPSC.h>
40
41/* #define CONMAJ_LOGGING 1 */
42#define quad_prog_tol 1e-4
43
44/*
45 * Use gradient-projection to solve Variable Placement with Separation Constraints problem.
46 */
47int
48constrained_majorization_vpsc(CMajEnvVPSC * e, float *b, float *place,
49 int max_iterations)
50{
51 int i, j, counter;
52 float *g, *old_place, *d;
53 /* for laplacian computation need number of real vars and those
54 * dummy vars included in lap
55 */
56 int n = e->nv + e->nldv;
57 bool converged = false;
58#ifdef CONMAJ_LOGGING
59 static int call_no = 0;
60#endif /* CONMAJ_LOGGING */
61
62 if (max_iterations == 0)
63 return 0;
64 g = e->fArray1;
65 old_place = e->fArray2;
66 d = e->fArray3;
67 if (e->m > 0) {
68 for (i = 0; i < n; i++) {
69 setVariableDesiredPos(e->vs[i], place[i]);
70 }
71 satisfyVPSC(e->vpsc);
72 for (i = 0; i < n; i++) {
73 place[i] = getVariablePos(e->vs[i]);
74 }
75 }
76#ifdef CONMAJ_LOGGING
77 float prev_stress = 0;
78 for (i = 0; i < n; i++) {
79 prev_stress += 2 * b[i] * place[i];
80 for (j = 0; j < n; j++) {
81 prev_stress -= e->A[i][j] * place[j] * place[i];
82 }
83 }
84 FILE *logfile = fopen("constrained_majorization_log", "a");
85#endif
86
87 for (counter = 0; counter < max_iterations && !converged; counter++) {
88 float test = 0;
89 float alpha, beta;
90 float numerator = 0, denominator = 0, r;
91 converged = true;
92 /* find steepest descent direction */
93 for (i = 0; i < n; i++) {
94 old_place[i] = place[i];
95 g[i] = 2 * b[i];
96 for (j = 0; j < n; j++) {
97 g[i] -= 2 * e->A[i][j] * place[j];
98 }
99 }
100 for (i = 0; i < n; i++) {
101 numerator += g[i] * g[i];
102 r = 0;
103 for (j = 0; j < n; j++) {
104 r += 2 * e->A[i][j] * g[j];
105 }
106 denominator -= r * g[i];
107 }
108 if (denominator != 0)
109 alpha = numerator / denominator;
110 else
111 alpha = 1.0;
112 for (i = 0; i < n; i++) {
113 place[i] -= alpha * g[i];
114 }
115 if (e->m > 0) {
116 /* project to constraint boundary */
117 for (i = 0; i < n; i++) {
118 setVariableDesiredPos(e->vs[i], place[i]);
119 }
120 satisfyVPSC(e->vpsc);
121 for (i = 0; i < n; i++) {
122 place[i] = getVariablePos(e->vs[i]);
123 }
124 }
125 /* set place to the intersection of old_place-g and boundary and
126 * compute d, the vector from intersection pnt to projection pnt
127 */
128 for (i = 0; i < n; i++) {
129 d[i] = place[i] - old_place[i];
130 }
131 /* now compute beta */
132 numerator = 0, denominator = 0;
133 for (i = 0; i < n; i++) {
134 numerator += g[i] * d[i];
135 r = 0;
136 for (j = 0; j < n; j++) {
137 r += 2 * e->A[i][j] * d[j];
138 }
139 denominator += r * d[i];
140 }
141 if (denominator != 0.0)
142 beta = numerator / denominator;
143 else
144 beta = 1.0;
145
146 for (i = 0; i < n; i++) {
147 /* beta > 1.0 takes us back outside the feasible region
148 * beta < 0 clearly not useful and may happen due to numerical imp.
149 */
150 if (beta > 0 && beta < 1.0) {
151 place[i] = old_place[i] + beta * d[i];
152 }
153 test += fabsf(place[i] - old_place[i]);
154 }
155#ifdef CONMAJ_LOGGING
156 float stress = 0;
157 for (i = 0; i < n; i++) {
158 stress += 2 * b[i] * place[i];
159 for (j = 0; j < n; j++) {
160 stress -= e->A[i][j] * place[j] * place[i];
161 }
162 }
163 fprintf(logfile, "%d: stress=%f, test=%f, %s\n", call_no, stress,
164 test, (stress >= prev_stress) ? "No Improvement" : "");
165 prev_stress = stress;
166#endif
167 if (test > quad_prog_tol) {
168 converged = false;
169 }
170 }
171#ifdef CONMAJ_LOGGING
172 call_no++;
173 fclose(logfile);
174#endif
175 return counter;
176}
177
178/*
179 * Set up environment and global constraints (dir-edge constraints, containment constraints
180 * etc).
181 *
182 * diredges: 0=no dir edge constraints
183 * 1=one separation constraint for each edge (in acyclic subgraph)
184 * 2=DiG-CoLa level constraints
185 */
186CMajEnvVPSC *initCMajVPSC(int n, float *packedMat, vtx_data * graph,
187 ipsep_options * opt, int diredges)
188{
189 int i;
190 /* nv is the number of real nodes */
191 int nConCs;
192 CMajEnvVPSC *e = gv_alloc(sizeof(CMajEnvVPSC));
193 e->A = NULL;
194 /* if we have clusters then we'll need two constraints for each var in
195 * a cluster */
196 e->nldv = 2 * opt->clusters.nclusters;
197 e->nv = n - e->nldv;
198 e->ndv = 0;
199
200 e->gcs = NULL;
201 e->vs = gv_calloc(n, sizeof(Variable*));
202 for (i = 0; i < n; i++) {
203 e->vs[i] = newVariable(i, 1.0, 1.0);
204 }
205 e->gm = 0;
206 if (diredges == 1) {
207 if (Verbose)
208 fprintf(stderr, " generate edge constraints...\n");
209 for (i = 0; i < e->nv; i++) {
210 for (size_t j = 1; j < graph[i].nedges; j++) {
211 if (graph[i].edists[j] > 0.01) {
212 e->gm++;
213 }
214 }
215 }
216 e->gcs = newConstraints(e->gm);
217 e->gm = 0;
218 for (i = 0; i < e->nv; i++) {
219 for (size_t j = 1; j < graph[i].nedges; j++) {
220 int u = i, v = graph[i].edges[j];
221 if (graph[i].edists[j] > 0) {
222 e->gcs[e->gm++] =
223 newConstraint(e->vs[u], e->vs[v], opt->edge_gap);
224 }
225 }
226 }
227 } else if (diredges == 2) {
228 int *ordering = NULL, *ls = NULL, cvar;
229 double halfgap;
230 DigColaLevel *levels;
231 Variable **vs = e->vs;
232 /* e->ndv is the number of dummy variables required, one for each boundary */
233 if (compute_hierarchy(graph, e->nv, 1e-2, 1e-1, NULL, &ordering, &ls,
234 &e->ndv)) return NULL;
235 levels = assign_digcola_levels(ordering, e->nv, ls, e->ndv);
236 if (Verbose)
237 fprintf(stderr, "Found %d DiG-CoLa boundaries\n", e->ndv);
238 e->gm =
239 get_num_digcola_constraints(levels, e->ndv + 1) + e->ndv - 1;
240 e->gcs = newConstraints(e->gm);
241 e->gm = 0;
242 e->vs = gv_calloc(n + e->ndv, sizeof(Variable*));
243 for (i = 0; i < n; i++) {
244 e->vs[i] = vs[i];
245 }
246 free(vs);
247 /* create dummy vars */
248 for (i = 0; i < e->ndv; i++) {
249 /* dummy vars should have 0 weight */
250 cvar = n + i;
251 e->vs[cvar] = newVariable(cvar, 1.0, 0.000001);
252 }
253 halfgap = opt->edge_gap;
254 for (i = 0; i < e->ndv; i++) {
255 cvar = n + i;
256 /* outgoing constraints for each var in level below boundary */
257 for (int j = 0; j < levels[i].num_nodes; j++) {
258 e->gcs[e->gm++] =
259 newConstraint(e->vs[levels[i].nodes[j]], e->vs[cvar],
260 halfgap);
261 }
262 /* incoming constraints for each var in level above boundary */
263 for (int j = 0; j < levels[i + 1].num_nodes; j++) {
264 e->gcs[e->gm++] =
265 newConstraint(e->vs[cvar],
266 e->vs[levels[i + 1].nodes[j]], halfgap);
267 }
268 }
269 /* constraints between adjacent boundary dummy vars */
270 for (i = 0; i < e->ndv - 1; i++) {
271 e->gcs[e->gm++] =
272 newConstraint(e->vs[n + i], e->vs[n + i + 1], 0);
273 }
274 }
275 if (opt->clusters.nclusters > 0) {
276 Constraint **ecs = e->gcs;
277 nConCs = 2 * opt->clusters.nvars;
278 e->gcs = newConstraints(e->gm + nConCs);
279 for (i = 0; i < e->gm; i++) {
280 e->gcs[i] = ecs[i];
281 }
282 if (ecs != NULL)
283 deleteConstraints(0, ecs);
284 for (i = 0; i < opt->clusters.nclusters; i++) {
285 for (int j = 0; j < opt->clusters.clustersizes[i]; j++) {
286 Variable *v = e->vs[opt->clusters.clusters[i][j]];
287 Variable *cl = e->vs[e->nv + 2 * i];
288 Variable *cr = e->vs[e->nv + 2 * i + 1];
289 e->gcs[e->gm++] = newConstraint(cl, v, 0);
290 e->gcs[e->gm++] = newConstraint(v, cr, 0);
291 }
292 }
293 }
294
295 e->m = 0;
296 e->cs = NULL;
297 if (e->gm > 0) {
298 e->vpsc = newIncVPSC(n + e->ndv, e->vs, e->gm, e->gcs);
299 e->m = e->gm;
300 e->cs = e->gcs;
301 }
302 if (packedMat != NULL) {
303 e->A = unpackMatrix(packedMat, n);
304 }
305
306 e->fArray1 = gv_calloc(n, sizeof(float));
307 e->fArray2 = gv_calloc(n, sizeof(float));
308 e->fArray3 = gv_calloc(n, sizeof(float));
309 if (Verbose)
310 fprintf(stderr,
311 " initCMajVPSC done: %d global constraints generated.\n",
312 e->m);
313 return e;
314}
315
316void deleteCMajEnvVPSC(CMajEnvVPSC * e)
317{
318 int i;
319 if (e->A != NULL) {
320 free(e->A[0]);
321 free(e->A);
322 }
323 if (e->m > 0) {
324 deleteVPSC(e->vpsc);
325 if (e->cs != e->gcs && e->gcs != NULL)
326 deleteConstraints(0, e->gcs);
327 deleteConstraints(e->m, e->cs);
328 for (i = 0; i < e->nv + e->nldv + e->ndv; i++) {
329 deleteVariable(e->vs[i]);
330 }
331 free(e->vs);
332 }
333 free(e->fArray1);
334 free(e->fArray2);
335 free(e->fArray3);
336 free(e);
337}
338
339/* generate non-overlap constraints inside each cluster, including dummy
340 * nodes at bounds of cluster
341 * generate constraints again for top level nodes and clusters treating
342 * clusters as rectangles of dim (l,r,b,t)
343 * for each cluster map in-constraints to l out-constraints to r
344 *
345 * For now, we'll keep the global containment constraints already
346 * generated for each cluster, and simply generate non-overlap constraints
347 * for all nodes and then an additional set of non-overlap constraints for
348 * clusters that we'll map back to the dummy vars as above.
349 */
350void generateNonoverlapConstraints(CMajEnvVPSC * e,
351 float nsizeScale,
352 float **coords,
353 int k,
354 bool transitiveClosure,
355 ipsep_options * opt)
356{
357 Constraint **csol, **csolptr;
358 int i, j, mol = 0;
359 int n = e->nv + e->nldv;
360 boxf* bb = gv_calloc(n, sizeof(boxf));
361 bool genclusters = opt->clusters.nclusters > 0;
362 if (genclusters) {
363 /* n is the number of real variables, not dummy cluster vars */
364 n -= 2 * opt->clusters.nclusters;
365 }
366 if (k == 0) {
367 /* grow a bit in the x dimension, so that if overlap is resolved
368 * horizontally then it won't be considered overlapping vertically
369 */
370 nsizeScale *= 1.0001f;
371 }
372 for (i = 0; i < n; i++) {
373 bb[i].LL.x =
374 coords[0][i] - nsizeScale * opt->nsize[i].x / 2.0 -
375 opt->gap.x / 2.0;
376 bb[i].UR.x =
377 coords[0][i] + nsizeScale * opt->nsize[i].x / 2.0 +
378 opt->gap.x / 2.0;
379 bb[i].LL.y =
380 coords[1][i] - nsizeScale * opt->nsize[i].y / 2.0 -
381 opt->gap.y / 2.0;
382 bb[i].UR.y =
383 coords[1][i] + nsizeScale * opt->nsize[i].y / 2.0 +
384 opt->gap.y / 2.0;
385 }
386 if (genclusters) {
387 Constraint ***cscl = gv_calloc(opt->clusters.nclusters + 1,
388 sizeof(Constraint**));
389 int* cm = gv_calloc(opt->clusters.nclusters + 1, sizeof(int));
390 for (i = 0; i < opt->clusters.nclusters; i++) {
391 int cn = opt->clusters.clustersizes[i];
392 Variable** cvs = gv_calloc(cn + 2, sizeof(Variable*));
393 boxf* cbb = gv_calloc(cn + 2, sizeof(boxf));
394 /* compute cluster bounding bb */
395 boxf container;
396 container.LL.x = container.LL.y = DBL_MAX;
397 container.UR.x = container.UR.y = -DBL_MAX;
398 for (j = 0; j < cn; j++) {
399 int iv = opt->clusters.clusters[i][j];
400 cvs[j] = e->vs[iv];
401 B2BF(bb[iv], cbb[j]);
402 EXPANDBB(container, bb[iv]);
403 }
404 B2BF(container, opt->clusters.bb[i]);
405 cvs[cn] = e->vs[n + 2 * i];
406 cvs[cn + 1] = e->vs[n + 2 * i + 1];
407 B2BF(container, cbb[cn]);
408 B2BF(container, cbb[cn + 1]);
409 if (k == 0) {
410 cbb[cn].UR.x = container.LL.x + 0.0001;
411 cbb[cn + 1].LL.x = container.UR.x - 0.0001;
412 cm[i] =
413 genXConstraints(cn + 2, cbb, cvs, &cscl[i],
414 transitiveClosure);
415 } else {
416 cbb[cn].UR.y = container.LL.y + 0.0001;
417 cbb[cn + 1].LL.y = container.UR.y - 0.0001;
418 cm[i] = genYConstraints(cn + 2, cbb, cvs, &cscl[i]);
419 }
420 mol += cm[i];
421 free (cvs);
422 free (cbb);
423 }
424 /* generate top level constraints */
425 {
426 int cn = opt->clusters.ntoplevel + opt->clusters.nclusters;
427 Variable** cvs = gv_calloc(cn, sizeof(Variable*));
428 boxf* cbb = gv_calloc(cn, sizeof(boxf));
429 for (i = 0; i < opt->clusters.ntoplevel; i++) {
430 int iv = opt->clusters.toplevel[i];
431 cvs[i] = e->vs[iv];
432 B2BF(bb[iv], cbb[i]);
433 }
434 /* make dummy variables for clusters */
435 for (i = opt->clusters.ntoplevel; i < cn; i++) {
436 cvs[i] = newVariable(123 + i, 1, 1);
437 j = i - opt->clusters.ntoplevel;
438 B2BF(opt->clusters.bb[j], cbb[i]);
439 }
440 i = opt->clusters.nclusters;
441 if (k == 0) {
442 cm[i] =
443 genXConstraints(cn, cbb, cvs, &cscl[i],
444 transitiveClosure);
445 } else {
446 cm[i] = genYConstraints(cn, cbb, cvs, &cscl[i]);
447 }
448 /* remap constraints from tmp dummy vars to cluster l and r vars */
449 for (i = opt->clusters.ntoplevel; i < cn; i++) {
450 double dgap;
451 j = i - opt->clusters.ntoplevel;
452 /* dgap is the change in required constraint gap.
453 * since we are going from a source rectangle the size
454 * of the cluster bounding box to a zero width (in x dim,
455 * zero height in y dim) rectangle, the change will be
456 * half the bb width.
457 */
458 if (k == 0) {
459 dgap = -(cbb[i].UR.x - cbb[i].LL.x) / 2.0;
460 } else {
461 dgap = -(cbb[i].UR.y - cbb[i].LL.y) / 2.0;
462 }
463 remapInConstraints(cvs[i], e->vs[n + 2 * j], dgap);
464 remapOutConstraints(cvs[i], e->vs[n + 2 * j + 1], dgap);
465 /* there may be problems with cycles between
466 * cluster non-overlap and diredge constraints,
467 * to resolve:
468 *
469 * for each constraint c:v->cvs[i]:
470 * if exists diredge constraint u->v where u in c:
471 * remap v->cl to cr->v (gap = height(v)/2)
472 *
473 * in = getInConstraints(cvs[i])
474 * for(c : in) {
475 * assert(c.right==cvs[i]);
476 * vin = getOutConstraints(v=c.left)
477 * for(d : vin) {
478 * if(d.left.cluster==i):
479 * tmp = d.left
480 * d.left = d.right
481 * d.right = tmp
482 * d.gap = height(d.right)/2
483 * }
484 * }
485 *
486 */
487 deleteVariable(cvs[i]);
488 }
489 mol += cm[opt->clusters.nclusters];
490 free (cvs);
491 free (cbb);
492 }
493 csolptr = csol = newConstraints(mol);
494 for (i = 0; i < opt->clusters.nclusters + 1; i++) {
495 /* copy constraints into csol */
496 for (j = 0; j < cm[i]; j++) {
497 *csolptr++ = cscl[i][j];
498 }
499 deleteConstraints(0, cscl[i]);
500 }
501 free (cscl);
502 free (cm);
503 } else {
504 if (k == 0) {
505 mol = genXConstraints(n, bb, e->vs, &csol, transitiveClosure);
506 } else {
507 mol = genYConstraints(n, bb, e->vs, &csol);
508 }
509 }
510 /* remove constraints from previous iteration */
511 if (e->m > 0) {
512 /* can't reuse instance of VPSC when constraints change! */
513 deleteVPSC(e->vpsc);
514 for (i = e->gm; i < e->m; i++) {
515 /* delete previous overlap constraints */
516 deleteConstraint(e->cs[i]);
517 }
518 /* just delete the array, not the elements */
519 if (e->cs != e->gcs)
520 deleteConstraints(0, e->cs);
521 }
522 /* if we have no global constraints then the overlap constraints
523 * are all we have to worry about.
524 * Otherwise, we have to copy the global and overlap constraints
525 * into the one array
526 */
527 if (e->gm == 0) {
528 e->m = mol;
529 e->cs = csol;
530 } else {
531 e->m = mol + e->gm;
532 e->cs = newConstraints(e->m);
533 for (i = 0; i < e->m; i++) {
534 if (i < e->gm) {
535 e->cs[i] = e->gcs[i];
536 } else {
537 e->cs[i] = csol[i - e->gm];
538 }
539 }
540 /* just delete the array, not the elements */
541 deleteConstraints(0, csol);
542 }
543 if (Verbose)
544 fprintf(stderr, " generated %d constraints\n", e->m);
545 e->vpsc = newIncVPSC(e->nv + e->nldv + e->ndv, e->vs, e->m, e->cs);
546 free (bb);
547}
548
549/*
550 * Statically remove overlaps, that is remove all overlaps by moving each node as
551 * little as possible.
552 */
553void removeoverlaps(int n, float **coords, ipsep_options * opt)
554{
555 int i;
556 CMajEnvVPSC *e = initCMajVPSC(n, NULL, NULL, opt, 0);
557 generateNonoverlapConstraints(e, 1.0, coords, 0, true, opt);
558 solveVPSC(e->vpsc);
559 for (i = 0; i < n; i++) {
560 coords[0][i] = getVariablePos(e->vs[i]);
561 }
562 generateNonoverlapConstraints(e, 1.0, coords, 1, false, opt);
563 solveVPSC(e->vpsc);
564 for (i = 0; i < n; i++) {
565 coords[1][i] = getVariablePos(e->vs[i]);
566 }
567 deleteCMajEnvVPSC(e);
568}
569
570/*
571 unpack the "ordering" array into an array of DigColaLevel
572*/
573DigColaLevel *assign_digcola_levels(int *ordering, int n, int *level_inds,
574 int num_divisions)
575{
576 int i, j;
577 DigColaLevel *l = gv_calloc(num_divisions + 1, sizeof(DigColaLevel));
578 /* first level */
579 l[0].num_nodes = level_inds[0];
580 l[0].nodes = gv_calloc(l[0].num_nodes, sizeof(int));
581 for (i = 0; i < l[0].num_nodes; i++) {
582 l[0].nodes[i] = ordering[i];
583 }
584 /* second through second last level */
585 for (i = 1; i < num_divisions; i++) {
586 l[i].num_nodes = level_inds[i] - level_inds[i - 1];
587 l[i].nodes = gv_calloc(l[i].num_nodes, sizeof(int));
588 for (j = 0; j < l[i].num_nodes; j++) {
589 l[i].nodes[j] = ordering[level_inds[i - 1] + j];
590 }
591 }
592 /* last level */
593 if (num_divisions > 0) {
594 l[num_divisions].num_nodes = n - level_inds[num_divisions - 1];
595 l[num_divisions].nodes = gv_calloc(l[num_divisions].num_nodes, sizeof(int));
596 for (i = 0; i < l[num_divisions].num_nodes; i++) {
597 l[num_divisions].nodes[i] =
598 ordering[level_inds[num_divisions - 1] + i];
599 }
600 }
601 return l;
602}
603
604/*********************
605get number of separation constraints based on the number of nodes in each level
606ie, num_sep_constraints = sum_i^{num_levels-1} (|L[i]|+|L[i+1]|)
607**********************/
608int get_num_digcola_constraints(DigColaLevel * levels, int num_levels)
609{
610 int i, nc = 0;
611 for (i = 1; i < num_levels; i++) {
612 nc += levels[i].num_nodes + levels[i - 1].num_nodes;
613 }
614 nc += levels[0].num_nodes + levels[num_levels - 1].num_nodes;
615 return nc;
616}
617
618#endif /* IPSEPCOLA */
Memory allocation wrappers that exit on failure.
static void * gv_calloc(size_t nmemb, size_t size)
Definition alloc.h:26
static void * gv_alloc(size_t size)
Definition alloc.h:47
void setVariableDesiredPos(Variable *v, double desiredPos)
Variable * newVariable(int id, double desiredPos, double weight)
Bridge for C programs to access solve_VPSC (which is in C++)
Constraint ** newConstraints(int m)
void deleteConstraint(Constraint *c)
VPSC * newIncVPSC(int n, Variable *vs[], int m, Constraint *cs[])
void deleteVariable(Variable *v)
void deleteConstraints(int m, Constraint **cs)
Constraint * newConstraint(Variable *left, Variable *right, double gap)
void remapInConstraints(Variable *u, Variable *v, double dgap)
int genXConstraints(int n, boxf *bb, Variable **vs, Constraint ***cs, bool transitiveClosure)
void remapOutConstraints(Variable *u, Variable *v, double dgap)
double getVariablePos(const Variable *v)
void satisfyVPSC(VPSC *vpsc)
int genYConstraints(int n, boxf *bb, Variable **vs, Constraint ***cs)
void deleteVPSC(VPSC *vpsc)
void solveVPSC(VPSC *vpsc)
#define B2BF(b, bf)
Definition geom.h:75
#define EXPANDBB(b0, b1)
Definition geom.h:57
static bool Verbose
Definition gml2gv.c:23
void free(void *)
node NULL
Definition grammar.y:163
Agraph_t * graph(char *name)
Definition gv.cpp:30
#define alpha
Definition shapes.c:4058
A constraint determines a minimum or exact spacing required between two variables.
Definition constraint.h:25
Definition geom.h:41
pointf UR
Definition geom.h:41
pointf LL
Definition geom.h:41
double x
Definition geom.h:29
double y
Definition geom.h:29