Skip to content

Commit 559e521

Browse files
committed
[SymForce] Set the default epsilon to a symbol for codegen
This PR does a couple things: 1) Add checks to `Codegen` for attempting to generate code with the default epsilon set to 0. What to do when this happens is configurable in the `CodegenConfig`. Options are warn (the default), raise, or proceed silently. 2) In the test mixin, set the behavior to raise. This means if you create a codegen test that does not set the default epsilon to a symbol, you'll get an exception. You could additionally override this by specifying in a particular `CodegenConfig` you create within the test `zero_epsilon_behavior=ALLOW`. 3) Set the default epsilon to the symbol for all codegen tests. We've been impressively good at passing epsilons where required, so there are minimal changes to the generated code from this. 4) Add helpful error messages, and a `symforce.set_epsilon_to_invalid()` method that is helpful for tracking down exactly where an epsilon was missing in very large expressions. Reviewers: dominic,nathan,harrison,hayk,bradley Topic: sym-default-epsilon-symbol Relative: GitOrigin-RevId: 76573d6c36d2940a48ea8dadea942ff68b40a537
1 parent 9a20a81 commit 559e521

30 files changed

+194
-15
lines changed

gen/cpp/sym/rot3.cc

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/python/sym/rot3.py

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

symforce/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,18 @@ def set_epsilon_to_zero() -> None:
315315
See `symforce.symbolic.epsilon` for more information.
316316
"""
317317
_set_epsilon(0.0)
318+
319+
320+
def set_epsilon_to_invalid() -> None:
321+
"""
322+
Set the default epsilon for SymForce to `None`. Should not be used to actually create
323+
expressions or generate code.
324+
325+
This is useful if you've forgotten to pass an epsilon somewhere, but are not sure where - using
326+
this epsilon in an expression should throw a `TypeError` near the location where you forgot to
327+
pass an epsilon.
328+
329+
This must be called before `symforce.symbolic` or other symbolic libraries have been imported.
330+
See `symforce.symbolic.epsilon` for more information.
331+
"""
332+
_set_epsilon(None)

symforce/cam/spherical_camera_cal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def pixel_from_camera_point(
191191

192192
# compute theta
193193
xy_norm = point[:2, :].norm(epsilon)
194-
theta = sf.atan2(xy_norm, point[2])
194+
theta = sf.atan2(xy_norm, point[2], epsilon=0)
195195
is_valid = sf.Max(sf.sign(self.critical_theta - theta), 0)
196196

197197
# clamp theta to critical_theta

symforce/codegen/backends/cpp/cpp_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class CppConfig(CodegenConfig):
2424
use_eigen_types: Use eigen_lcm types for vectors instead of lists
2525
autoformat: Run a code formatter on the generated code
2626
cse_optimizations: Optimizations argument to pass to sf.cse
27+
zero_epsilon_behavior: What should codegen do if a default epsilon is not set?
2728
support_complex: Generate code that can work with std::complex or with regular float types
2829
force_no_inline: Mark generated functions as `__attribute__((noinline))`
2930
zero_initialization_sparsity_threshold: Threshold between 0 and 1 for the sparsity below

symforce/codegen/backends/python/python_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class PythonConfig(CodegenConfig):
2424
use_eigen_types: Use eigen_lcm types for vectors instead of lists
2525
autoformat: Run a code formatter on the generated code
2626
cse_optimizations: Optimizations argument to pass to sf.cse
27+
zero_epsilon_behavior: What should codegen do if a default epsilon is not set?
2728
use_numba: Add the `@numba.njit` decorator to generated functions. This will greatly
2829
speed up functions by compiling them to machine code, but has large overhead
2930
on the first call and some overhead on subsequent calls, so it should not be

symforce/codegen/codegen.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,39 @@ def __init__(
9595
docstring: The docstring to be used with the generated function
9696
"""
9797

98+
if sf.epsilon() == 0:
99+
warning_message = """
100+
Generating code with epsilon set to 0 - This is dangerous! You may get NaNs, Infs,
101+
or numerically unstable results from calling generated functions near singularities.
102+
103+
In order to safely generate code, you should set epsilon to either a symbol
104+
(recommended) or a small numerical value like `sf.numeric_epsilon`. You should do
105+
this before importing any other code from symforce, e.g. with
106+
107+
import symforce
108+
symforce.set_epsilon_to_symbol()
109+
110+
or
111+
112+
import symforce
113+
symforce.set_epsilon_to_number()
114+
115+
For more information on use of epsilon to prevent singularities, take a look at the
116+
Epsilon Tutorial: https://symforce.org/tutorials/epsilon_tutorial.html
117+
"""
118+
warning_message = textwrap.indent(textwrap.dedent(warning_message), " ")
119+
120+
if config.zero_epsilon_behavior == codegen_config.ZeroEpsilonBehavior.FAIL:
121+
raise ValueError(warning_message)
122+
elif config.zero_epsilon_behavior == codegen_config.ZeroEpsilonBehavior.WARN:
123+
logger.warning(warning_message)
124+
elif config.zero_epsilon_behavior == codegen_config.ZeroEpsilonBehavior.ALLOW:
125+
pass
126+
else:
127+
raise ValueError(
128+
f"Invalid config.zero_epsilon_behavior: {config.zero_epsilon_behavior}"
129+
)
130+
98131
self.name = name
99132

100133
# Inputs and outputs must be Values objects
@@ -111,9 +144,42 @@ def __init__(
111144
# All symbols in outputs must be present in inputs
112145
input_symbols_list = codegen_util.flat_symbols_from_values(inputs)
113146
input_symbols = set(input_symbols_list)
114-
assert self.output_symbols.issubset(
115-
input_symbols
116-
), f"A symbol in the output expression is missing from inputs. inputs={input_symbols}"
147+
if not self.output_symbols.issubset(input_symbols):
148+
missing_outputs = self.output_symbols - input_symbols
149+
error_msg = textwrap.dedent(
150+
f"""
151+
A symbol in the output expression is missing from inputs
152+
153+
Inputs:
154+
{input_symbols}
155+
156+
Missing symbols:
157+
{self.output_symbols - input_symbols}
158+
"""
159+
)
160+
161+
if sf.epsilon() in missing_outputs:
162+
error_msg += textwrap.dedent(
163+
f"""
164+
One of the missing symbols is `{sf.epsilon()}`, which is the default epsilon -
165+
this typically means you called a function that requires an epsilon without
166+
passing a value. You need to either pass 0 for epsilon if you'd like to use 0,
167+
pass through the symbol you're using for epsilon if it's not `{sf.epsilon()}`,
168+
or add `{sf.epsilon()}` as an input to your generated function. You would do
169+
this either by adding an argument `{sf.epsilon()}: sf.Scalar` if using a
170+
symbolic function, or setting `inputs["{sf.epsilon()}"] = sf.Symbol("{sf.epsilon()}")`
171+
if using `inputs` and `outputs` `Values`.
172+
173+
If you aren't sure where you may have forgotten to pass an epsilon, setting
174+
epsilon to invalid may be helpful. You should do this before importing any other
175+
code from symforce, e.g. with
176+
177+
import symforce
178+
symforce.set_epsilon_to_invalid()
179+
"""
180+
)
181+
182+
raise ValueError(error_msg)
117183

118184
# Names given by keys in inputs/outputs must be valid variable names
119185
# TODO(aaron): Also check recursively

symforce/codegen/codegen_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# ----------------------------------------------------------------------------
55
from abc import abstractmethod
66
from dataclasses import dataclass
7+
from dataclasses import field
8+
from enum import Enum
79
from pathlib import Path
810

911
from sympy.printing.codeprinter import CodePrinter
@@ -13,6 +15,21 @@
1315
CURRENT_DIR = Path(__file__).parent
1416

1517

18+
class ZeroEpsilonBehavior(Enum):
19+
"""
20+
Options for what to do when attempting to generate code with the default epsilon set to 0
21+
"""
22+
23+
FAIL = 0
24+
WARN = 1
25+
ALLOW = 2
26+
27+
28+
# Default for new codegen configs - this lets you modify the default for all configs, e.g. for all
29+
# codegen tests
30+
DEFAULT_ZERO_EPSILON_BEHAVIOR = ZeroEpsilonBehavior.WARN
31+
32+
1633
# TODO(hayk): Address this type ignore, which comes from having abstract methods on a dataclass.
1734
@dataclass # type: ignore
1835
class CodegenConfig:
@@ -26,6 +43,7 @@ class CodegenConfig:
2643
use_eigen_types: Use eigen_lcm types for vectors instead of lists
2744
autoformat: Run a code formatter on the generated code
2845
cse_optimizations: Optimizations argument to pass to sf.cse
46+
zero_epsilon_behavior: What should codegen do if a default epsilon is not set?
2947
"""
3048

3149
doc_comment_line_prefix: str
@@ -35,6 +53,9 @@ class CodegenConfig:
3553
cse_optimizations: T.Optional[
3654
T.Union[T.Literal["basic"], T.Sequence[T.Tuple[T.Callable, T.Callable]]]
3755
] = None
56+
zero_epsilon_behavior: ZeroEpsilonBehavior = field(
57+
default_factory=lambda: DEFAULT_ZERO_EPSILON_BEHAVIOR
58+
)
3859

3960
@classmethod
4061
@abstractmethod

symforce/codegen/geo_package_codegen.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ def codegen_mul(group: T.Type, multiplicand_type: T.Type) -> Codegen:
9999
config=config,
100100
),
101101
Codegen.function(
102-
func=lambda self: sf.V3(self.to_yaw_pitch_roll()),
102+
# TODO(aaron): We currently can't generate custom methods with defaults - fix this, and
103+
# pass epsilon as an argument with a default
104+
func=lambda self: sf.V3(self.to_yaw_pitch_roll(epsilon=0)),
103105
input_types=[sf.Rot3],
104106
name="to_yaw_pitch_roll",
105107
config=config,

symforce/internal/symbolic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def foo(x: Scalar, epsilon: Scalar = sf.epsilon()) -> Scalar:
271271
in the SymForce docs here: https://symforce.org/tutorials/epsilon_tutorial.html
272272
273273
For purely numerical code that just needs a good default numerical epsilon, see
274-
`symforce.symbolic.numerical_epsilon`.
274+
`symforce.symbolic.numeric_epsilon`.
275275
276276
Returns: The current default epsilon. This is typically some kind of "Scalar", like a float or
277277
a Symbol.

0 commit comments

Comments
 (0)