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