Skip to content

Commit 553f041

Browse files
committed
Fix Turn GeneralOffset curvature and rare BSpline discretization tolerance violation
1 parent c4d3ef3 commit 553f041

File tree

3 files changed

+58
-31
lines changed

3 files changed

+58
-31
lines changed

src/paths/segments/offset.jl

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct ConstantOffset{T, S} <: OffsetSegment{T, S}
2424
end
2525

2626
"""
27-
struct ConstantOffset{T,S} <: OffsetSegment{T,S}
27+
struct GeneralOffset{T,S} <: OffsetSegment{T,S}
2828
"""
2929
struct GeneralOffset{T, S} <: OffsetSegment{T, S}
3030
seg::S
@@ -67,7 +67,7 @@ end
6767

6868
function tangent(s::OffsetSegment, t)
6969
# calculate derivative w.r.t. s.seg arclength
70-
# d/dt (s(t) + offset(t) * normal(t))
70+
# d/dt (s.seg(t) + offset(s, t) * normal(s.seg, t))
7171
base_dir = direction(s.seg, t)
7272
base_tangent = Point(cos(base_dir), sin(base_dir))
7373
base_normal = Point(-base_tangent.y, base_tangent.x)
@@ -84,38 +84,46 @@ function signed_curvature(seg::Segment, s)
8484
end
8585

8686
function curvatureradius(seg::ConstantOffset, s)
87-
r = curvatureradius(seg.seg, s)
87+
r = curvatureradius(seg.seg, s) # Ignore offset * dκds term
8888
return r - getoffset(seg, s)
8989
end
9090

9191
function curvatureradius(seg::GeneralOffset, s)
92-
# This one is only approximate for BSpline offsets
93-
# let s be seg.seg arclength, let t be seg arclength
94-
# tangent(seg, s) defined above is dseg/ds
95-
# ||d^2/dt^2 seg(s)|| = ||d/dt tangent(seg, s) ds/dt||
96-
# ||d/ds tangent(seg, s)|| ||ds/dt||^2
97-
# Where ||ds/dt|| = ||tangent(seg, s)|| = 1 for non-offset segments
92+
# Only approximate for BSpline offsets (ignores offset * dκds term)
93+
# let s (above argument) be seg.seg arclength, let l be seg arclength
94+
# g is dseg/ds, defined above as `tangent(seg, s)`
95+
# ||d^2/dl^2 seg(s)|| = ||d/dl (g ds/dl)||
96+
# ||(d/ds g) (ds/dl)^2 + g d^2s/dl^2||
97+
# Where ||ds/dl|| = 1/||g|| = 1 for non-offset segments
9898
# But needs a correction for offsets
99-
# dt/ds = κ n # Sorry t is tangent for the rest of this comment
99+
# dt/ds = κ n # t is tangent, n normal, κ curvature of base curve
100100
# dn/ds = -κ t
101-
# || κ n + (d2_offset n - κ t d_offset) - d_offset κ t - offset κ^2 n - offset t dκ/ds ||
101+
# ||(κ n + (d2_offset n - κ t d_offset) - d_offset κ t - offset κ^2 n - offset t dκ/ds)(ds/dl)^2 + tangent(seg, s) d^2s/dl^2||
102102

103103
r = curvatureradius(seg.seg, s)
104-
reparam = 1 / (sqrt((1 - getoffset(seg, s) / r)^2 + offset_derivative(seg, s)^2))
104+
ds_dl = 1 / (sqrt((1 - getoffset(seg, s) / r)^2 + offset_derivative(seg, s)^2))
105105
base_dir = direction(seg.seg, s)
106106
base_tangent = Point(cos(base_dir), sin(base_dir)) # True for non-offset segments (arclength parameterized)
107107
base_normal = Point(-base_tangent.y, base_tangent.x)
108108

109109
d_offset = offset_derivative(seg, s)
110110
d2_offset = ForwardDiff.derivative(s_ -> offset_derivative(seg, s_), s)
111+
d2s_dl2 =
112+
(-1 / 2) *
113+
ds_dl^3 *
114+
(
115+
-2 * (offset_derivative(seg, s) / r) * (1 - getoffset(seg, s) / r) +
116+
2 * offset_derivative(seg, s) * d2_offset
117+
)
118+
111119
d2_seg =
112120
(
113121
(1 / r) * base_normal +
114122
d2_offset * base_normal +
115123
-2 * (1 / r) * base_tangent * d_offset +
116124
-((1 / r) * base_normal) * ((1 / r) * getoffset(seg, s))
117-
# - getoffset(seg, s) * base_tangent * dκds # Nonzero for BSplines but basically fine to ignore
118-
) * reparam^2
125+
#- getoffset(seg, s) * base_tangent * dκds(seg.seg, s) # Nonzero for BSplines but basically fine to ignore
126+
) * ds_dl^2 + tangent(seg, s) * d2s_dl2
119127
return sign(r) / norm(d2_seg)
120128
end
121129

@@ -174,6 +182,8 @@ end
174182
function arclength(f::ConstantOffset{T, BSpline{T}}, t1::Real=1.0; t0::Real=0.0) where {T}
175183
check_interval(f, t0, t1)
176184
l_orig = arclength(f.seg, t1; t0=t0)
185+
# Constant offset just changes tangent component of gradient
186+
# So we can just add the extra contribution due to curvature
177187
G = StaticArrays.@MVector [zero(Point{T})]
178188
H = StaticArrays.@MVector [zero(Point{T})]
179189
l_extra = f.offset * quadgk(t -> _curvature_arclength!(f.seg, G, H, t), t0, t1)[1]
@@ -183,23 +193,23 @@ end
183193
# For general offsets we also have to add the contribution from varying offset
184194
function arclength(f::GeneralOffset{T, BSpline{T}}, t1::Real=1.0; t0::Real=0.0) where {T}
185195
check_interval(f, t0, t1)
186-
# If we unwind the curvature, each offset segment will have length (sqrt(1+x^2) ds)
187-
# where x is the derivative of the offset
188-
l_orig = quadgk(
189-
s -> sqrt(1 + (ForwardDiff.derivative(f.offset, s))^2),
190-
t_to_arclength(f.seg, t0),
191-
t_to_arclength(f.seg, t1)
192-
)[1]
193-
# Now add curvature contribution as usual
194196
G = StaticArrays.@MVector [zero(Point{T})]
195197
H = StaticArrays.@MVector [zero(Point{T})]
196-
l_extra = quadgk(
198+
# Each offset segment will have length (sqrt(s′^2 +x^2) ds)
199+
# where x is the derivative of the offset (normal component of arclength)
200+
# and s′ is the curvature*offset contribution (tangent arclength)
201+
l = quadgk(
197202
t ->
198-
_curvature_arclength!(f.seg, G, H, t) * getoffset(f, t_to_arclength(f.seg, t)),
203+
sqrt(
204+
(
205+
1 -
206+
_curvature!(f.seg, G, H, t) * getoffset(f, t_to_arclength(f.seg, t))
207+
)^2 + (offset_derivative(f, t_to_arclength(f.seg, t)))^2
208+
) * dsdt(t, f.seg.r),
199209
t0,
200210
t1
201211
)[1]
202-
return l_orig + l_extra # + because of _curvature_arclength! sign convention
212+
return l
203213
end
204214

205215
function _curvature_arclength!(b::BSpline, G, H, t)
@@ -211,6 +221,13 @@ function _curvature_arclength!(b::BSpline, G, H, t)
211221
return uconvert(NoUnits, (G[1].y * H[1].x - G[1].x * H[1].y) / norm(G[1])^2)
212222
end
213223

224+
function _curvature!(b::BSpline, G, H, t)
225+
Paths.Interpolations.gradient!(G, b.r, t)
226+
Paths.Interpolations.hessian!(H, b.r, t)
227+
return uconvert(NoUnits, (G[1].x * H[1].y - G[1].y * H[1].x) / norm(G[1])^2) /
228+
norm(G[1])
229+
end
230+
214231
function _split(seg::ConstantOffset, x)
215232
segs = split(seg.seg, x)
216233
return offset(segs[1], seg.offset), offset(segs[2], seg.offset)

src/utils.jl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,10 +204,12 @@ function discretization_grid(s::Paths.Segment, tolerance)
204204
end
205205

206206
function discretization_grid(s::Paths.BSpline, tolerance)
207+
# Assume ds/dt ≈ 1 to avoid converting between arclength and t
207208
h(t) = Paths.Interpolations.hessian(s.r, t)[1]
208209
return discretization_grid(h, tolerance)
209210
end
210211

212+
# Discretize using marching algorithm based on Hessian or curvature
211213
function discretization_grid(
212214
ddf,
213215
tolerance,
@@ -219,20 +221,28 @@ function discretization_grid(
219221
ts[1] = bnds[1]
220222
t = bnds[1]
221223
i = 1
224+
cc = norm(ddf(t))
222225
while t < bnds[2]
223226
i = i + 1
224227
i > length(ts) && error(
225228
"Too many points in curve for GDS. Increase discretization tolerance or split the curve."
226229
)
227230
t = ts[i - 1]
228-
cc = norm(ddf(t))
229-
if cc >= 1e-9 * oneunit(typeof(cc))
231+
# Set dt based on distance from chord assuming constant curvature
232+
if cc >= 1e-9 * oneunit(typeof(cc)) # Update dt if curvature is not near zero
230233
dt = uconvert(NoUnits, sqrt(8 * tolerance / cc) / t_scale)
231-
# Also assumes third derivative is small
232234
end
233235
if t + dt >= bnds[2]
234236
dt = bnds[2] - t
235237
end
238+
# Check that curvature didn't increase too much (decrease is fine)
239+
# Rare but may happen near inflection points
240+
cc_next = norm(ddf(t + dt))
241+
if (t_scale * dt)^2 * (cc_next - cc) / 24 > tolerance
242+
dt = uconvert(NoUnits, sqrt(8 * tolerance / cc_next) / t_scale)
243+
cc_next = norm(ddf(t + dt))
244+
end
245+
cc = cc_next
236246
ts[i] = min(bnds[2], t + dt)
237247
t = ts[i]
238248
end

test/test_bspline.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ end
136136
g = g_fd(c, s, ds)
137137
h = h_fd(c, s, ds)
138138
((g.x^2 + g.y^2)^(3 // 2)) / (g.x * h.y - g.y * h.x)
139-
end
139+
end # assumes constant d(arclength)/ds
140140
# For BSplines, curvature radius calculation is only approximate, but not bad
141141
c = curv[1].curves[1] # ConstantOffset BSpline
142142
@test abs(curvatureradius_fd(c, 10μm) - Paths.curvatureradius(c, 10μm)) < 1nm
143143
c = curv[3].curves[1] # GeneralOffset BSpline
144144
@test abs(curvatureradius_fd(c, 10μm) - Paths.curvatureradius(c, 10μm)) < 50nm
145145
c = curv[5].curves[1] # GeneralOffset Turn
146-
# Paths.curvatureradius is exact for Turn offsets, the finite difference is wrong
147-
@test abs(curvatureradius_fd(c, 10μm) - Paths.curvatureradius(c, 10μm)) < 600nm
146+
# Paths.curvatureradius is exact for Turn offsets
147+
@test abs(curvatureradius_fd(c, 10μm) - Paths.curvatureradius(c, 10μm)) < 1nm
148148
approx = Paths.bspline_approximation(c, atol=100.0nm)
149149
pts = DeviceLayout.discretize_curve(c, 100.0nm)
150150
pts_approx = DeviceLayout.discretize_curve(approx, 100.0nm)

0 commit comments

Comments
 (0)