# ----------------------------------------------------------------------------# SymForce - Copyright 2022, Skydio, Inc.# This source code is under the Apache 2.0 license found in the LICENSE file.# ----------------------------------------------------------------------------fromabcimportABCfromabcimportabstractmethodfromsymforceimportpython_utilfromsymforceimporttypingasTfromsymforce.ops.impl.dataclass_storage_opsimportDataclassStorageOpsfromsymforce.valuesimportValues
[docs]classSubProblem(ABC):""" A logical grouping of a set of variables and objective terms that use those variables Typical usage is to subclass SubProblem, and define an :attr:`Inputs` dataclass on your subclass for any variables provided by the subproblem. Then define :meth:`build_residuals`, which should return a :class:`symforce.values.values.Values` where each leaf is a :class:`.residual_block.ResidualBlock`, representing the residuals for your subproblem. For example:: class MySubProblem(SubProblem): @dataclass class Inputs: x: sf.Scalar pose: sf.Pose3 objective_params: MyObjective.Params # Optional, but helpful for type checking inputs: MySubProblem.Inputs def build_residuals(self) -> Values: residual_blocks = Values() residual_blocks["my_objective"] = MyObjective.residual( self.inputs.x, self.inputs.pose, self.inputs.objective_params ) return residual_blocks SubProblems can also depend on variables or expressions from other subproblems; the recommended way to do this is to add arguments to :meth:`build_residuals` for any expressions your subproblem needs from other subproblems. Both :attr:`Inputs` and :meth:`build_residuals` must be defined, but can be empty - a SubProblem can be just a set of variables with no objectives (for example, variables that are used in other subproblems). It can also be a set of objectives with no variables, i.e. with all of its inputs coming from other subproblems. Args: name: (optional) The name of the subproblem, derived from the class name by default """Inputs:T.Type[T.Dataclass]name:strinputs:T.Dataclassdef__init__(self,name:T.Optional[str]=None):self.name=nameorself._default_name()assertself.name,"SubProblem name cannot be empty"self.build_inputs()
[docs]defbuild_inputs(self)->None:""" Build the inputs block of the subproblem, and store in :attr:`self.inputs <inputs>`. The default implementation works for fixed-size Dataclasses; for dynamic-size dataclasses, or to customize this, override this function. """self.inputs=DataclassStorageOps.symbolic(self.Inputs,name=self.name)
[docs]@T.any_args@abstractmethoddefbuild_residuals(self,*args:T.Any)->Values:""" Build the residual blocks for the subproblem, and return as a Values. Each SubProblem subclass should define this. Typically, the SubProblem implementation of this function will take additional arguments, for expressions coming from other SubProblem dependencies or other hyperparameters. Returns: residual_blocks: A Values of any structure, but where each leaf is a ResidualBlock """pass
[docs]@abstractmethoddefoptimized_values(self)->T.List[T.Any]:""" Return the list of optimized values for this subproblem. Each entry should be a leaf-level object in the subproblem :attr:`Inputs` """pass
@classmethoddef_default_name(cls)->str:""" Pick the default name for a SubProblem class by using the class name, minus the SubProblem suffix if it exists. Returns: name: The subproblem name """returnpython_util.camelcase_to_snakecase(python_util.str_removesuffix(cls.__name__,"SubProblem"))