Skip to content

Commit f63171c

Browse files
[SymForce] Fixed default epsilon
With this, we add two methods to `symforce.sympy`: - `sm.default_epsilon`, and - `sm.set_default_epsilon`. With these, the user can set the value returned by `sm.default_epsilon` to be any value they'd like using `sm.set_default_epsilon`. Currently, if the user doesn't set the default epsilon, I default it to `0.0` (this could be trivially changed though). `sm.set_default_epsilon` can only be called if the `sm.default_epsilon` hasn't yet been called (raises an `AlreadyUsedDefaultEpsilon` exception). This is to ensure that the default epsilons are consistent between different functions (as I imagine it would lead to confusing behavior if that weren't enforced). Topic: fixed_default_epsilon Relative: use_default_epsilon GitOrigin-RevId: 73757807f84f4e9430d84740b3fdfe8a6a479421
1 parent 90c4891 commit f63171c

File tree

1 file changed

+61
-21
lines changed

1 file changed

+61
-21
lines changed

symforce/initialization.py

+61-21
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def modify_symbolic_api(sympy_module: T.Any) -> None:
3737
override_count_ops(sympy_module)
3838
override_matrix_symbol(sympy_module)
3939
add_derivatives(sympy_module)
40+
add_epsilon_functions(sympy_module)
4041

4142
setattr(sympy_module, "_MODIFIED_BY_SYMFORCE", True)
4243

@@ -300,38 +301,77 @@ def override_count_ops(sympy_module: T.Type) -> None:
300301
sympy_module.count_ops = _sympy_count_ops.count_ops
301302

302303

303-
def add_custom_methods(sympy_module: T.Type) -> None:
304+
class AlreadyUsedEpsilon(Exception):
305+
pass
306+
307+
308+
def add_epsilon_functions(sympy_module: T.Type) -> None:
304309
"""
305-
Add safe helper methods to the symbolic API.
310+
Add sympy_module.epsilon() and associated functions.
311+
312+
Precondition: This function can be called on a given module only once.
306313
"""
307314

308-
def register(func: T.Callable) -> T.Callable:
309-
setattr(sympy_module, func.__name__, func)
310-
return func
311-
312-
# Should match C++ default epsilon in epsilon.h
313-
sympy_module.numeric_epsilon = 10 * sys.float_info.epsilon
315+
EPSILON = 0.0
316+
HAVE_USED_EPSILON = False
314317

315318
# Save original functions to reference in wrappers
316319
original_atan2 = sympy_module.atan2
317320

318-
@register
321+
def add_custom_methods_with_epsilon(sympy_module: T.Type, default_epsilon: T.Scalar) -> None:
322+
def atan2(y: T.Scalar, x: T.Scalar, epsilon: T.Scalar = default_epsilon) -> T.Scalar:
323+
return original_atan2(y, x + (sympy_module.sign(x) + 0.5) * epsilon)
324+
325+
sympy_module.atan2 = atan2
326+
327+
def asin_safe(x: T.Scalar, epsilon: T.Scalar = default_epsilon) -> T.Scalar:
328+
x_safe = sympy_module.Max(-1 + epsilon, sympy_module.Min(1 - epsilon, x))
329+
return sympy_module.asin(x_safe)
330+
331+
sympy_module.asin_safe = asin_safe
332+
333+
def acos_safe(x: T.Scalar, epsilon: T.Scalar = default_epsilon) -> T.Scalar:
334+
x_safe = sympy_module.Max(-1 + epsilon, sympy_module.Min(1 - epsilon, x))
335+
return sympy_module.acos(x_safe)
336+
337+
sympy_module.acos_safe = acos_safe
338+
339+
add_custom_methods_with_epsilon(sympy_module, EPSILON)
340+
319341
def epsilon() -> T.Scalar:
320-
return 0
342+
nonlocal HAVE_USED_EPSILON
343+
HAVE_USED_EPSILON = True
344+
return EPSILON
321345

322-
@register
323-
def atan2(y: T.Scalar, x: T.Scalar, epsilon: T.Scalar = epsilon()) -> T.Scalar:
324-
return original_atan2(y, x + (sympy_module.sign(x) + 0.5) * epsilon)
346+
sympy_module.epsilon = epsilon
325347

326-
@register
327-
def asin_safe(x: T.Scalar, epsilon: T.Scalar = epsilon()) -> T.Scalar:
328-
x_safe = sympy_module.Max(-1 + epsilon, sympy_module.Min(1 - epsilon, x))
329-
return sympy_module.asin(x_safe)
348+
def set_epsilon(new_epsilon: T.Scalar) -> None:
349+
nonlocal HAVE_USED_EPSILON
350+
if HAVE_USED_EPSILON:
351+
raise AlreadyUsedEpsilon(
352+
"Cannot set return value of epsilon after it has already been called."
353+
)
330354

331-
@register
332-
def acos_safe(x: T.Scalar, epsilon: T.Scalar = epsilon()) -> T.Scalar:
333-
x_safe = sympy_module.Max(-1 + epsilon, sympy_module.Min(1 - epsilon, x))
334-
return sympy_module.acos(x_safe)
355+
# NOTE(brad): These must be redefined so they use new_epsilon
356+
add_custom_methods_with_epsilon(sympy_module, default_epsilon=new_epsilon)
357+
358+
nonlocal EPSILON
359+
EPSILON = new_epsilon
360+
361+
sympy_module.set_epsilon = set_epsilon
362+
363+
364+
def add_custom_methods(sympy_module: T.Type) -> None:
365+
"""
366+
Add safe helper methods to the symbolic API.
367+
"""
368+
369+
def register(func: T.Callable) -> T.Callable:
370+
setattr(sympy_module, func.__name__, func)
371+
return func
372+
373+
# Should match C++ default epsilon in epsilon.h
374+
sympy_module.numeric_epsilon = 10 * sys.float_info.epsilon
335375

336376
@register
337377
def wrap_angle(x: T.Scalar) -> T.Scalar:

0 commit comments

Comments
 (0)