forgekernel 0.9.1__tar.gz → 0.9.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {forgekernel-0.9.1/src/forgekernel.egg-info → forgekernel-0.9.2}/PKG-INFO +1 -1
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/__init__.py +1 -1
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/brep.py +17 -29
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/exact.py +9 -16
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/kernel.py +63 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/surd.py +54 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2/src/forgekernel.egg-info}/PKG-INFO +1 -1
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel.egg-info/SOURCES.txt +1 -0
- forgekernel-0.9.2/tests/test_rotate.py +87 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/LICENSE +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/README.md +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/pyproject.toml +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/setup.cfg +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/bsolid.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/csg.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/curve.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/hlr.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/interval.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/io.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/loft.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/mesh2d.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/nurbs.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/profile2d.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/quadric.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/ssi.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/stepio.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/surfacing.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/tess.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel/trim.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel.egg-info/dependency_links.txt +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel.egg-info/requires.txt +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/src/forgekernel.egg-info/top_level.txt +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_hlr.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k1_exact.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k37_loft.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k38_profile.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k3_curves.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k3_ssi.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k3_step.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k6_surfacing.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k7_bsolid.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k7_stepexport.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k7_trim.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_k7_trimflux.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_rust_oracle.py +0 -0
- {forgekernel-0.9.1 → forgekernel-0.9.2}/tests/test_scaffold.py +0 -0
|
@@ -188,25 +188,21 @@ class Solid:
|
|
|
188
188
|
moment) and closure requires the SIGNED interval coverage on every
|
|
189
189
|
line to cancel exactly. Zero everywhere == closed surface."""
|
|
190
190
|
from collections import defaultdict
|
|
191
|
-
from math import gcd as _gcd
|
|
192
191
|
|
|
193
192
|
def canon_dir(d: Vec) -> Vec | None:
|
|
194
|
-
|
|
193
|
+
# Canonical (scale- and sign-invariant) representative of a
|
|
194
|
+
# direction: divide through by the first nonzero component so any
|
|
195
|
+
# scalar multiple ±λ·d maps to the same tuple. Works whether the
|
|
196
|
+
# components are rational (ℚ) or quadratic surds (ℚ[√d]) — an
|
|
197
|
+
# exactly-rotated solid carries √d edge directions.
|
|
198
|
+
lead = None
|
|
195
199
|
for v in d:
|
|
196
|
-
den = den * v.denominator // _gcd(den, v.denominator)
|
|
197
|
-
ints = [int(v * den) for v in d]
|
|
198
|
-
g = 0
|
|
199
|
-
for v in ints:
|
|
200
|
-
g = _gcd(g, abs(v))
|
|
201
|
-
if g == 0:
|
|
202
|
-
return None
|
|
203
|
-
ints = [v // g for v in ints]
|
|
204
|
-
for v in ints:
|
|
205
200
|
if v != 0:
|
|
206
|
-
|
|
207
|
-
ints = [-w for w in ints]
|
|
201
|
+
lead = v
|
|
208
202
|
break
|
|
209
|
-
|
|
203
|
+
if lead is None:
|
|
204
|
+
return None
|
|
205
|
+
return tuple(v / lead for v in d)
|
|
210
206
|
|
|
211
207
|
lines: dict = defaultdict(list)
|
|
212
208
|
for p in self.polys:
|
|
@@ -298,24 +294,16 @@ def _ear_clip(loop: list[tuple]) -> list[tuple]:
|
|
|
298
294
|
|
|
299
295
|
|
|
300
296
|
def _canon_dir(d: Vec):
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
# Scale- and sign-invariant direction key: divide by the first nonzero
|
|
298
|
+
# component. Rational (ℚ) or quadratic-surd (ℚ[√d], from exact rotation).
|
|
299
|
+
lead = None
|
|
304
300
|
for v in d:
|
|
305
|
-
den = den * v.denominator // _g(den, v.denominator)
|
|
306
|
-
ints = [int(v * den) for v in d]
|
|
307
|
-
g = 0
|
|
308
|
-
for v in ints:
|
|
309
|
-
g = _g(g, abs(v))
|
|
310
|
-
if g == 0:
|
|
311
|
-
return None
|
|
312
|
-
ints = [v // g for v in ints]
|
|
313
|
-
for v in ints:
|
|
314
301
|
if v != 0:
|
|
315
|
-
|
|
316
|
-
ints = [-w for w in ints]
|
|
302
|
+
lead = v
|
|
317
303
|
break
|
|
318
|
-
|
|
304
|
+
if lead is None:
|
|
305
|
+
return None
|
|
306
|
+
return tuple(v / lead for v in d)
|
|
319
307
|
|
|
320
308
|
|
|
321
309
|
def logical_edges(solid: Solid) -> list[dict]:
|
|
@@ -87,24 +87,17 @@ class Plane:
|
|
|
87
87
|
return Plane(neg(self.n), -self.d)
|
|
88
88
|
|
|
89
89
|
def canonical(self) -> tuple:
|
|
90
|
-
"""Hashable canonical form: normal
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
g = 0
|
|
98
|
-
for v in ints:
|
|
99
|
-
g = gcd(g, abs(v))
|
|
100
|
-
if g:
|
|
101
|
-
ints = [v // g for v in ints]
|
|
102
|
-
for v in ints[:3]:
|
|
90
|
+
"""Hashable canonical form: (normal, d) divided through by the normal's
|
|
91
|
+
first nonzero component, so any scalar multiple ±λ·(n, d) maps to the
|
|
92
|
+
same tuple — coplanarity becomes tuple equality. Works over ℚ and over
|
|
93
|
+
ℚ[√d] (an exactly-rotated solid carries surd face normals)."""
|
|
94
|
+
nums = (self.n[0], self.n[1], self.n[2], self.d)
|
|
95
|
+
lead = None
|
|
96
|
+
for v in nums[:3]:
|
|
103
97
|
if v != 0:
|
|
104
|
-
|
|
105
|
-
ints = [-w for w in ints]
|
|
98
|
+
lead = v
|
|
106
99
|
break
|
|
107
|
-
return tuple(
|
|
100
|
+
return tuple(v / lead for v in nums)
|
|
108
101
|
|
|
109
102
|
def coplanar_key(self) -> tuple:
|
|
110
103
|
"""Canonical form ignoring orientation (for adjacency grouping)."""
|
|
@@ -51,6 +51,69 @@ def rotate_quarter(s: Solid, axis: str, quarters: int) -> Solid:
|
|
|
51
51
|
return s.rotated_quarter(axis, quarters)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _cos_sin_deg(deg):
|
|
55
|
+
"""Exact (cos, sin) of an angle in degrees, in ℚ[√d] — or None if the angle
|
|
56
|
+
needs a larger field. Covers the multiples of 30° and 45° (so circular
|
|
57
|
+
patterns of 3/4/6/8/12 and the diagonal sketch-plane rotations are exact)."""
|
|
58
|
+
from forgekernel.surd import SurdVal
|
|
59
|
+
|
|
60
|
+
from fractions import Fraction
|
|
61
|
+
|
|
62
|
+
d = int(deg) % 360
|
|
63
|
+
if deg != int(deg):
|
|
64
|
+
return None
|
|
65
|
+
if d % 90 == 0:
|
|
66
|
+
return [(F(1), F(0)), (F(0), F(1)), (F(-1), F(0)), (F(0), F(-1))][d // 90]
|
|
67
|
+
h = Fraction(1, 2)
|
|
68
|
+
r3, r2 = SurdVal(0, h, 3), SurdVal(0, h, 2) # √3/2, √2/2
|
|
69
|
+
table = {30: (r3, h), 60: (h, r3), 120: (-h, r3), 150: (-r3, h),
|
|
70
|
+
210: (-r3, -h), 240: (-h, -r3), 300: (h, -r3), 330: (r3, -h),
|
|
71
|
+
45: (r2, r2), 135: (-r2, r2), 225: (-r2, -r2), 315: (r2, -r2)}
|
|
72
|
+
return table.get(d)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _rotation_matrix(axis, deg):
|
|
76
|
+
"""Exact Rodrigues rotation matrix (list of 3 rows) about ``axis`` by
|
|
77
|
+
``deg``, over ℚ[√d]. Raises ValueError if the angle is not representable."""
|
|
78
|
+
from forgekernel.surd import SurdVal, sqrt_rational
|
|
79
|
+
|
|
80
|
+
cs = _cos_sin_deg(deg)
|
|
81
|
+
if cs is None:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"rotation by {deg}° is not exactly representable in ℚ[√d] "
|
|
84
|
+
"(only multiples of 30° and 45°)")
|
|
85
|
+
c, s = cs
|
|
86
|
+
ax, ay, az = F(axis[0]), F(axis[1]), F(axis[2])
|
|
87
|
+
n2 = ax * ax + ay * ay + az * az # |axis|², rational
|
|
88
|
+
if n2 == 0:
|
|
89
|
+
raise ValueError("zero rotation axis")
|
|
90
|
+
one = SurdVal(1, 0, 1)
|
|
91
|
+
s_over_n = (s if isinstance(s, SurdVal) else SurdVal(s)) / sqrt_rational(n2)
|
|
92
|
+
k2c = (one - (c if isinstance(c, SurdVal) else SurdVal(c))) / n2
|
|
93
|
+
k = [[F(0), -az, ay], [az, F(0), -ax], [-ay, ax, F(0)]] # [axis]×
|
|
94
|
+
k2 = [[sum(k[i][t] * k[t][j] for t in range(3)) for j in range(3)]
|
|
95
|
+
for i in range(3)]
|
|
96
|
+
return [[(F(1) if i == j else F(0)) + s_over_n * k[i][j] + k2c * k2[i][j]
|
|
97
|
+
for j in range(3)] for i in range(3)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def rotate(s: Solid, axis, deg) -> Solid:
|
|
101
|
+
"""Exact rotation of a planar solid about an arbitrary axis by an angle in
|
|
102
|
+
ℚ[√d] (multiples of 30°/45°). Rigid motion — volume is preserved exactly and
|
|
103
|
+
the result is a Solid with exact ℚ[√d] coordinates. Raises ValueError for
|
|
104
|
+
angles a larger field would be needed for (the seam turns that into an
|
|
105
|
+
honest refusal)."""
|
|
106
|
+
r = _rotation_matrix(axis, deg)
|
|
107
|
+
|
|
108
|
+
def fn(v):
|
|
109
|
+
x, y, z = v
|
|
110
|
+
return (r[0][0] * x + r[0][1] * y + r[0][2] * z,
|
|
111
|
+
r[1][0] * x + r[1][1] * y + r[1][2] * z,
|
|
112
|
+
r[2][0] * x + r[2][1] * y + r[2][2] * z)
|
|
113
|
+
|
|
114
|
+
return s.mapped(fn)
|
|
115
|
+
|
|
116
|
+
|
|
54
117
|
def chamfer(s: Solid, distance) -> Solid:
|
|
55
118
|
"""Exact chamfer on all convex rational-normal edges, with industrial
|
|
56
119
|
corner-triangle vertex truncation (oracle-matched semantics)."""
|
|
@@ -76,6 +76,9 @@ class SurdVal:
|
|
|
76
76
|
o = self._co(o)
|
|
77
77
|
return SurdVal(self.a - o.a, self.b - o.b, self._radical(o))
|
|
78
78
|
|
|
79
|
+
def __rsub__(self, o) -> "SurdVal": # o − self, for rational/int on the left
|
|
80
|
+
return self._co(o) - self
|
|
81
|
+
|
|
79
82
|
def __mul__(self, o) -> "SurdVal":
|
|
80
83
|
o = self._co(o)
|
|
81
84
|
if self.b == 0 or o.b == 0:
|
|
@@ -89,12 +92,63 @@ class SurdVal:
|
|
|
89
92
|
|
|
90
93
|
__rmul__ = __mul__
|
|
91
94
|
|
|
95
|
+
def __neg__(self) -> "SurdVal":
|
|
96
|
+
return SurdVal(-self.a, -self.b, self.d)
|
|
97
|
+
|
|
98
|
+
def __truediv__(self, o) -> "SurdVal":
|
|
99
|
+
if isinstance(o, (int, Fraction)):
|
|
100
|
+
r = F(o)
|
|
101
|
+
return SurdVal(self.a / r, self.b / r, self.d)
|
|
102
|
+
o = self._co(o)
|
|
103
|
+
if o.b == 0: # divide by a rational
|
|
104
|
+
return SurdVal(self.a / o.a, self.b / o.a, self.d)
|
|
105
|
+
# divide by (c+e√d) via the conjugate: ·(c−e√d)/(c²−e²d)
|
|
106
|
+
denom = o.a * o.a - o.b * o.b * o.d # rational, nonzero
|
|
107
|
+
num = self * SurdVal(o.a, -o.b, o.d)
|
|
108
|
+
return SurdVal(num.a / denom, num.b / denom, num.d)
|
|
109
|
+
|
|
110
|
+
def __rtruediv__(self, o) -> "SurdVal":
|
|
111
|
+
return self._co(o) / self
|
|
112
|
+
|
|
113
|
+
def _sign(self) -> int:
|
|
114
|
+
"""Exact sign of a + b√d (d ≥ 1, √d > 0) — decides comparisons."""
|
|
115
|
+
if self.b == 0:
|
|
116
|
+
return (self.a > 0) - (self.a < 0)
|
|
117
|
+
if self.a == 0:
|
|
118
|
+
return (self.b > 0) - (self.b < 0)
|
|
119
|
+
sa = (self.a > 0) - (self.a < 0)
|
|
120
|
+
sb = (self.b > 0) - (self.b < 0)
|
|
121
|
+
if sa == sb: # both terms same sign
|
|
122
|
+
return sa
|
|
123
|
+
# opposite signs: compare magnitudes a² vs b²·d (squaring is monotone)
|
|
124
|
+
da, db = self.a * self.a, self.b * self.b * self.d
|
|
125
|
+
if da == db:
|
|
126
|
+
return 0
|
|
127
|
+
return sa if da > db else sb
|
|
128
|
+
|
|
129
|
+
def __lt__(self, o) -> bool:
|
|
130
|
+
return (self - self._co(o))._sign() < 0
|
|
131
|
+
|
|
132
|
+
def __le__(self, o) -> bool:
|
|
133
|
+
return (self - self._co(o))._sign() <= 0
|
|
134
|
+
|
|
135
|
+
def __gt__(self, o) -> bool:
|
|
136
|
+
return (self - self._co(o))._sign() > 0
|
|
137
|
+
|
|
138
|
+
def __ge__(self, o) -> bool:
|
|
139
|
+
return (self - self._co(o))._sign() >= 0
|
|
140
|
+
|
|
92
141
|
def __eq__(self, o: object) -> bool:
|
|
93
142
|
o = self._co(o)
|
|
94
143
|
if self.b == 0 and o.b == 0:
|
|
95
144
|
return self.a == o.a
|
|
96
145
|
return self.a == o.a and self.b == o.b and self.d == o.d
|
|
97
146
|
|
|
147
|
+
def __hash__(self) -> int:
|
|
148
|
+
if self.b == 0: # a pure rational hashes as itself
|
|
149
|
+
return hash(self.a)
|
|
150
|
+
return hash((self.a, self.b, self.d))
|
|
151
|
+
|
|
98
152
|
def __float__(self) -> float:
|
|
99
153
|
return float(self.a) + float(self.b) * math.sqrt(self.d)
|
|
100
154
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Exact rotation over ℚ[√d] — the K2.2 circular-pattern / diagonal-sketch-plane
|
|
2
|
+
enabler. Rotation is a rigid motion, so volume is preserved EXACTLY (rational),
|
|
3
|
+
and representable angles (multiples of 30°/45°) keep coordinates exact."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from forgekernel import kernel as fk
|
|
12
|
+
from forgekernel.surd import SurdVal, sqrt_rational
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_rotation_preserves_volume_exactly() -> None:
|
|
16
|
+
b = fk.box(10, 4, 3)
|
|
17
|
+
for axis, deg in [((0, 0, 1), 60), ((0, 0, 1), 45), ((1, 0, 0), 30),
|
|
18
|
+
((1, 1, 1), 120)]:
|
|
19
|
+
r = fk.rotate(b, axis, deg)
|
|
20
|
+
assert r.volume() == 120 # exact rational, not a float
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_six_sixty_degree_rotations_are_the_identity() -> None:
|
|
24
|
+
b = fk.box(7, 5, 2)
|
|
25
|
+
acc = b
|
|
26
|
+
for _ in range(6):
|
|
27
|
+
acc = fk.rotate(acc, (0, 0, 1), 60)
|
|
28
|
+
orig = {tuple(float(c) for c in v) for p in b.polys for v in p.verts}
|
|
29
|
+
back = {tuple(float(c) for c in v) for p in acc.polys for v in p.verts}
|
|
30
|
+
assert orig == back # 6·60° = 360° back to start, exactly
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_diagonal_120_is_a_rational_axis_permutation() -> None:
|
|
34
|
+
p = fk.rotate(fk.box(3, 5, 7), (1, 1, 1), 120)
|
|
35
|
+
# 120° about the body diagonal cyclically permutes the axes — the surds
|
|
36
|
+
# cancel, leaving purely rational coordinates.
|
|
37
|
+
assert all(not isinstance(c, SurdVal) or c.b == 0
|
|
38
|
+
for poly in p.polys for v in poly.verts for c in v)
|
|
39
|
+
assert p.volume() == 105
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_unrepresentable_angle_is_refused() -> None:
|
|
43
|
+
with pytest.raises(ValueError):
|
|
44
|
+
fk.rotate(fk.box(2, 2, 2), (0, 0, 1), 37)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_boolean_union_of_rotated_copies_is_watertight_and_exact() -> None:
|
|
48
|
+
# the circular-pattern path: union disjoint rotated copies (surd coords)
|
|
49
|
+
seed = fk.translate(fk.box(5, 2, 3), 10, -1, 0)
|
|
50
|
+
out = seed
|
|
51
|
+
for i in range(1, 6):
|
|
52
|
+
out = fk.boolean("union", out, fk.rotate(seed, (0, 0, 1), 60 * i))
|
|
53
|
+
assert out.volume() == 6 * 5 * 2 * 3 # disjoint -> exact sum, no leaks
|
|
54
|
+
assert out.watertight_violations() == []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_overlapping_boolean_mixes_rational_and_surd_coords() -> None:
|
|
58
|
+
# the case disjoint patterns miss: an axis-aligned (ℚ) body booleaned with
|
|
59
|
+
# an OVERLAPPING rotated (ℚ[√3]) body — BSP clipping makes mixed-type
|
|
60
|
+
# polygons, so Fraction−SurdVal must resolve (needs SurdVal.__rsub__).
|
|
61
|
+
from fractions import Fraction as F
|
|
62
|
+
|
|
63
|
+
a = fk.box(2, 2, 2)
|
|
64
|
+
b = fk.rotate(fk.translate(fk.box(2, 2, 2), F(1, 2), F(1, 2), 0), (0, 0, 1), 30)
|
|
65
|
+
vu = float(fk.boolean("union", a, b).volume())
|
|
66
|
+
vi = float(fk.boolean("intersect", a, b).volume())
|
|
67
|
+
vc = float(fk.boolean("cut", a, b).volume())
|
|
68
|
+
assert vi > 0 # they genuinely overlap
|
|
69
|
+
assert abs(vu + vi - 16) < 1e-9 # |A∪B| + |A∩B| = |A| + |B|
|
|
70
|
+
assert abs(vc - (8 - vi)) < 1e-9 # |A∖B| = |A| − |A∩B|
|
|
71
|
+
for op in ("union", "cut", "intersect"):
|
|
72
|
+
assert fk.boolean(op, a, b).watertight_violations() == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_surd_is_an_ordered_field() -> None:
|
|
76
|
+
import random
|
|
77
|
+
random.seed(0)
|
|
78
|
+
for _ in range(2000):
|
|
79
|
+
d = random.choice([2, 3, 5, 6])
|
|
80
|
+
from fractions import Fraction as F
|
|
81
|
+
x = SurdVal(F(random.randint(-9, 9), random.randint(1, 4)),
|
|
82
|
+
F(random.randint(-9, 9), random.randint(1, 4)), d)
|
|
83
|
+
y = SurdVal(F(random.randint(-9, 9), random.randint(1, 4)),
|
|
84
|
+
F(random.randint(-9, 9), random.randint(1, 4)), d)
|
|
85
|
+
if abs(float(x) - float(y)) > 1e-9:
|
|
86
|
+
assert (x < y) == (float(x) < float(y))
|
|
87
|
+
assert (x / y) * y == x if float(y) != 0 else True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|