# ----------------------------------------------------------------------------# SymForce - Copyright 2022, Skydio, Inc.# This source code is under the Apache 2.0 license found in the LICENSE file.# ----------------------------------------------------------------------------"""The top-level symforce packageImporting this by itself performs minimal initialization configuration, and the functions here aremostly for configuration purposes.In particular, this primarily performs configuration that you might need before importing:mod:`symforce.symbolic`."""importosimportsysimporttypingasTfromdataclassesimportdataclassfromtypesimportModuleType# -------------------------------------------------------------------------------------------------# Version# -------------------------------------------------------------------------------------------------# isort: splitfrom._versionimportversionas__version__# -------------------------------------------------------------------------------------------------# Logging configuration# -------------------------------------------------------------------------------------------------# isort: splitimportlogging# Create a logger with this print formatLOGGING_FORMAT="%(module)s.%(funcName)s():%(lineno)s%(levelname)s -- %(message)s"logging.basicConfig(format=LOGGING_FORMAT)logger=logging.getLogger(__package__)
[docs]defset_log_level(log_level:str)->None:""" Set symforce logger level. The default is INFO, but can be set by one of: 1) The SYMFORCE_LOGLEVEL environment variable 2) Calling this function before any other symforce imports Args: log_level: {DEBUG, INFO, WARNING, ERROR, CRITICAL} """# Set default log levelifnothasattr(logging,log_level.upper()):raiseRuntimeError(f'Unknown log level: "{log_level}"')logger.setLevel(getattr(logging,log_level.upper()))# Only do this if already imported, in case users don't want to use any C++ binariesif"cc_sym"insys.modules:importcc_symcc_sym.set_log_level(log_level)
# Set defaultset_log_level(os.environ.get("SYMFORCE_LOGLEVEL","INFO"))# -------------------------------------------------------------------------------------------------# Symbolic API configuration# -------------------------------------------------------------------------------------------------
[docs]classInvalidSymbolicApiError(Exception):def__init__(self,api:str):super().__init__(f'Symbolic API is "{api}", must be one of ("sympy", "symengine")')
def_find_symengine()->ModuleType:""" Attempts to import symengine from its location in the symforce build directory If symengine is already in sys.modules, will return that module. If symengine cannot be imported, raises ImportError. Returns the imported symengine module """if"symengine"insys.modules:returnsys.modules["symengine"]try:# If symengine is available on python path, use it# TODO(will, aaron): this might not be the version of symengine that we wantimportsymenginereturnsymengineexceptImportErrorasex:importimportlibimportimportlib.abcimportimportlib.utilfrom.importpath_utiltry:symengine_install_dir=path_util.symenginepy_install_dir()exceptpath_util.MissingManifestException:raiseImportError("Unable to import SymEngine, either installed or in the manifest.json")fromexsymengine_path_candidates=list(symengine_install_dir.glob("lib/python3*/site-packages/symengine/__init__.py"))+list(symengine_install_dir.glob("local/lib/python3*/dist-packages/symengine/__init__.py"))iflen(symengine_path_candidates)!=1:raiseImportError(f"Should be exactly one symengine package, found candidates {symengine_path_candidates} in directory {path_util.symenginepy_install_dir()}")fromexsymengine_path=symengine_path_candidates[0]# Import symengine from the directory where we installed it. See# https://docs.python.org/3/library/importlib.html#importing-a-source-file-directlyspec=importlib.util.spec_from_file_location("symengine",symengine_path)assertspecisnotNonesymengine=importlib.util.module_from_spec(spec)sys.modules["symengine"]=symengine# For mypy: https://github.com/python/typeshed/issues/2793assertisinstance(spec.loader,importlib.abc.Loader)try:spec.loader.exec_module(symengine)except:# If executing the module fails for any reason, it shouldn't be in `sys.modules`delsys.modules["symengine"]raisereturnsymengine_symbolic_api:T.Optional[T.Literal["sympy","symengine"]]=None_have_imported_symbolic=Falsedef_set_symbolic_api(sympy_module:T.Literal["sympy","symengine"])->None:# Set this as the default symbolic APIglobal_symbolic_api# noqa: PLW0603_symbolic_api=sympy_moduledef_use_symengine()->None:try:_find_symengine()exceptImportError:logger.critical("Commanded to use symengine, but failed to import.")raise_set_symbolic_api("symengine")def_use_sympy()->None:# Import just to make sure it's importable and fail here if it's not (as opposed to failing# later)importsympyassympy_py_set_symbolic_api("sympy")
[docs]defset_symbolic_api(name:str)->None:""" Set the symbolic API for symforce See the SymPy tutorial for information on the symbolic APIs that can be used: https://symforce.org/tutorials/sympy_tutorial.html By default, SymForce will use the ``symengine`` API if it is available. If the symbolic API is set to ``sympy`` it will use that. If ``symengine`` is not available and the symbolic API was not set, it will emit a warning and use the ``sympy`` API. The symbolic API can be set by one of: 1) The ``SYMFORCE_SYMBOLIC_API`` environment variable 2) Calling this function before any other symforce imports Args: name: {sympy, symengine} """if_have_imported_symbolicandname!=_symbolic_api:raiseValueError("The symbolic API cannot be changed after `symforce.symbolic` has been imported. ""Import the top-level `symforce` module and call `symforce.set_symbolic_api` before ""importing anything else!")if_symbolic_apiisnotNoneandname==_symbolic_api:logger.debug(f'already on symbolic API "{name}"')returnelse:logger.debug(f'symbolic API: "{name}"')ifname=="sympy":_use_sympy()elifname=="symengine":_use_symengine()else:raiseNotImplementedError(f'Unknown symbolic API: "{name}"')
# Set default to symengine if available, else sympyif"SYMFORCE_SYMBOLIC_API"inos.environ:set_symbolic_api(os.environ["SYMFORCE_SYMBOLIC_API"])else:try:_find_symengine()logger.debug("No SYMFORCE_SYMBOLIC_API set, found and using symengine.")set_symbolic_api("symengine")exceptImportError:logger.debug("No SYMFORCE_SYMBOLIC_API set, no symengine found. Will use sympy.")pass
[docs]defget_symbolic_api()->T.Literal["sympy","symengine"]:""" Return the current symbolic API as a string. """return_symbolic_apior"sympy"
# --------------------------------------------------------------------------------# Default epsilon# --------------------------------------------------------------------------------# Should match C++ default epsilon in epsilon.hnumeric_epsilon=10*sys.float_info.epsilon
[docs]classAlreadyUsedEpsilon(Exception):""" Exception thrown on attempting to modify the default epsilon after it has been used elsewhere """pass
_epsilon:T.Any=0.0_have_used_epsilon=Falsedef_set_epsilon(new_epsilon:T.Any)->None:""" Set the default epsilon for SymForce This must be called before :mod:`symforce.symbolic` or other symbolic libraries have been imported. Typically it should be set to some kind of Scalar, such as an int, float, or Symbol. See :func:`symforce.symbolic.epsilon` for more information. Args: new_epsilon: The new default epsilon to use """global_epsilon# noqa: PLW0603if_have_used_epsilonandnew_epsilon!=_epsilon:raiseAlreadyUsedEpsilon(f"Cannot set return value of epsilon to {new_epsilon} after it has already been "f"accessed with value {_epsilon}.")_epsilon=new_epsilon
[docs]@dataclassclassSymbolicEpsilon:""" An indicator that SymForce should use a symbolic epsilon """name:str
[docs]defset_epsilon_to_symbol(name:str="epsilon")->None:""" Set the default epsilon for Symforce to a Symbol. This must be called before :mod:`symforce.symbolic` or other symbolic libraries have been imported. See :func:`symforce.symbolic.epsilon` for more information. Args: name: The name of the symbol for the new default epsilon to use """_set_epsilon(SymbolicEpsilon(name))
[docs]defset_epsilon_to_number(value:T.Any=numeric_epsilon)->None:""" Set the default epsilon for Symforce to a number. This must be called before :mod:`symforce.symbolic` or other symbolic libraries have been imported. See :func:`symforce.symbolic.epsilon` for more information. Args: value: The new default epsilon to use """_set_epsilon(value)
[docs]defset_epsilon_to_zero()->None:""" Set the default epsilon for Symforce to zero. This must be called before :mod:`symforce.symbolic` or other symbolic libraries have been imported. See :func:`symforce.symbolic.epsilon` for more information. """_set_epsilon(0.0)
[docs]defset_epsilon_to_invalid()->None:""" Set the default epsilon for SymForce to ``None``. Should not be used to actually create expressions or generate code. This is useful if you've forgotten to pass an epsilon somewhere, but are not sure where - using this epsilon in an expression should throw a ``TypeError`` near the location where you forgot to pass an epsilon. This must be called before :mod:`symforce.symbolic` or other symbolic libraries have been imported. See :func:`symforce.symbolic.epsilon` for more information. """_set_epsilon(None)