Source code for cofi._inversion_options

import warnings
import difflib
from typing import Union, Type
from collections.abc import Callable
import json

from .tools import tool_suggest_table, tool_dispatch_table, solving_methods


[docs] class InversionOptions: r"""Class for specification on how an inversion will run, including which backend tool to use and solver-specific parameters. .. tip:: A typical workflow of :code:`InversionOptions`: Step 1 (optional): let the :ref:`Guidance Methods <guide>` to walk you through available solving methods in a hierarchical way. Step 2: Use the :ref:`Set/Unset Backend Tools <set_unset_tools>` to fix your choice on which backend tool to use. Step 3: Set solver-specific parameters using the :ref:`Solver Params <set_params>` related methods. .. admonition:: Example usage of InversionOptions :class: dropdown, attention >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.get_default_tool() 'scipy.optimize.minimize' >>> inv_options.suggest_tools() Here's a complete list of inversion tools supported by CoFI (grouped by methods): { "optimization": [ "scipy.optimize.minimize", "scipy.optimize.least_squares" ], "matrix solvers": [ "scipy.linalg.lstsq" ], "sampling": [ "emcee" ] } >>> inv_options.set_tool("scipy.linalg.lstsq") >>> inv_options.suggest_solver_params() Current backend tool scipy.linalg.lstsq has the following solver-specific parameters: Required parameters: -- nothing -- Optional parameters & default settings: {'cond': None, 'overwrite_a': False, 'overwrite_b': False, 'check_finite': True, 'lapack_driver': None} >>> inv_options.summary() ============================= Summary for inversion options ============================= Solving method: None set Use `suggest_solving_methods()` to check available solving methods. ----------------------------- Backend tool: `scipy.linalg.lstsq` - SciPy's wrapper function over LAPACK's linear least-squares solver, using 'gelsd', 'gelsy' (default), or 'gelss' as backend driver References: ['https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.lstsq.html', 'https://www.netlib.org/lapack/lug/node27.html'] Use `suggest_tools()` to check available backend tools. ----------------------------- Solver-specific parameters: None set Use `suggest_solver_params()` to check required/optional solver-specific parameters. .. warning:: Methods that guide users through available **solvers tree** is still under consideration - we are working on deciding how such APIs are named and used. Ideally, we have a tree in the backend, with the root level branching into ``sampling``, ``direct search`` and ``optimization`` and further categorisations that lead to lists of backend tools as the leaves. .. _guide: .. rubric:: Guidance Methods Here are how you can walk through our solvers tree as a guidance. Note that this step is optional, and you can always jump to :ref:`Set/Unset Backend Tools <set_unset_tools>` directly. .. autosummary:: InversionOptions.set_solving_method InversionOptions.unset_solving_method :ref:`back to top <top_InversionOptions>` .. _set_unset_tools: .. rubric:: Set/Unset Backend Tools To select/unselect a backend tool, use the following methods .. autosummary:: InversionOptions.set_tool InversionOptions.unset_tool InversionOptions.get_tool InversionOptions.get_default_tool InversionOptions.suggest_tools :ref:`back to top <top_InversionOptions>` .. _set_params: .. rubric:: Solver Params To set tool-specific parameters, use the following methods .. autosummary:: InversionOptions.set_params InversionOptions.get_params InversionOptions.suggest_solver_params :ref:`back to top <top_InversionOptions>` """ def __init__(self): self.hyper_params = {} self.tool = None self.method = None
[docs] def set_params(self, **kwargs): r"""Sets solver-specific parameters Use :func:`InversionOptions.suggest_solver_params` to get a list of parameters required and optional. To set the parameters, use argument keyword to specify directly which parameter you refer to. Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 9 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.suggest_solver_params() Current backend tool scipy.optimize.minimize (default) has the following solver-specific parameters: Required parameters: -- nothing -- Optional parameters & default settings: {'method': None, 'tol': None, 'callback': None, 'options': None} >>> inv_options.set_params(method="Nelder-Mead") """ self.hyper_params.update(kwargs)
[docs] def get_params(self) -> dict: r"""Get solver-specific parameters defined so far Returns ------- dict a Python dictionary that maps solver-specific parameter name to the value you've set. Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 4 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.set_params(method="Nelder-Mead") >>> inv_options.get_params() {'method': 'Nelder-Mead'} """ return self.hyper_params
[docs] def set_solving_method(self, method: str): r"""Sets the solving method .. warning:: The current version is a flattened version of our inference tools tree, we are going to change this interface very soon. Use :func:`InversionOptions.suggest_solving_methods` to get a list of solving methods to choose from. Parameters ---------- method : str the string that represents a solving approach Raises ------ ValueError when the solving method you attempt to set is invalid Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.set_solving_method("matrix solvers") >>> inv_options.suggest_tools() Based on the solving method you've set, the following tools are suggested: ['scipy.linalg.lstsq'] Use `InversionOptions.set_tool(tool_name)` to set a specific tool from above Use `InversionOptions.set_solving_method(method_name)` to change solving method Use `InversionOptions.unset_solving_method()` if you'd like to see more options Check CoFI documentation 'Advanced Usage' section for how to plug in your own solver """ if method is None: self.unset_solving_method() elif method in solving_methods: self.method = method else: close_matches = difflib.get_close_matches(method, solving_methods) _error_msg_suffix = ( f"\n\nDid you mean '{close_matches[0]}'?" if len(close_matches) else "" ) raise ValueError( "the solving method is invalid, please choose from" f" {solving_methods}{_error_msg_suffix}" )
[docs] def unset_solving_method(self): """Unsets the chosen solving approach""" self.method = None
[docs] def set_tool(self, tool: Union[str, Type]): r"""Sets the tool that will be the backend solver for your inversion problem This can be: - a backend tool we support, use :func:`InversionOptions.suggest_tools` to get a list of tools you can choose from - or your own solver class, check `our tutorial - Advanced Usage <tutorial.html#advanced-usage>`_ for details about how to define and use your custom inference tool Parameters ---------- tool : Union[str, Type] either the name of a backend tool or your custom :class:`tools.BaseInferenceTool` class Raises ------ ValueError when the string you pass in isn't in our supported tools list, or when the inference tool class you pass in doesn't implement the ``__call__(self,)`` method. Examples -------- .. admonition:: code example: set a supported tool :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.set_tool("scipy.linalg.lstsq") >>> inv_options.summary() ============================= Summary for inversion options ============================= Solving method: None set Use `suggest_solving_methods()` to check available solving methods. ----------------------------- Backend tool: `scipy.linalg.lstsq` - SciPy's wrapper function over LAPACK's linear least-squares solver, using 'gelsd', 'gelsy' (default), or 'gelss' as backend driver References: ['https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.lstsq.html', 'https://www.netlib.org/lapack/lug/node27.html'] Use `suggest_tools()` to check available backend tools. ----------------------------- Solver-specific parameters: None set Use `suggest_solver_params()` to check required/optional solver-specific parameters. .. admonition:: code example: set your own solver :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3-8, 11 >>> from cofi.tools import BaseInferenceTool >>> from cofi import InversionOptions >>> class MyDummySolver(BaseInferenceTool): ... short_description = "My dummy solver that always return (1,2) as result" ... def __init__(self, inv_problem, inv_options): ... super().__init__(inv_problem, inv_options) ... def __call__(self): ... return {"model": np.array([1,2]), "success": True} ... >>> inv_options = InversionOptions() >>> inv_options.set_tool(MyDummySolver) >>> inv_options.summary() ============================= Summary for inversion options ============================= Solving method: None set Use `suggest_solving_methods()` to check available solving methods. ----------------------------- Backend tool: `<class '__main__.MyDummySolver'>` - My dummy solver that always return (1,2) as result References: [] Use `suggest_tools()` to check available backend tools. ----------------------------- Solver-specific parameters: None set Use `suggest_solver_params()` to check required/optional solver-specific parameters. """ if tool is None: self.unset_tool() elif isinstance(tool, Type): if ( issubclass(tool, Callable) and "__call__" not in tool.__abstractmethods__ ): self.tool = tool else: raise ValueError( "the custom solver class you've provided should implement" " __call__(self,) methodread CoFI documentation 'tutorials -" " Advanced Usage' section for how to plug inyour own solver" ) else: if tool not in tool_dispatch_table: close_matches = difflib.get_close_matches( tool, tool_dispatch_table.keys() ) _error_msg_suffix = ( f"\n\nDid you mean '{close_matches[0]}'?" if len(close_matches) else "" ) raise ValueError( "the tool is invalid, please use" " `InversionOptions.suggest_tools()` to see" f" options{_error_msg_suffix}" ) if self.method and tool not in tool_suggest_table[self.method]: warnings.warn( f"the tool {tool} is valid but doesn't match the solving method" f" you've selected: {self.method}" ) self.tool = tool
[docs] def unset_tool(self): """Unsets the chosen backend tool""" self.tool = None
[docs] def get_tool(self) -> Union[str, Type]: """Get the backend tool chosen so far, or the default tool if not chosen Returns ------- Union[str, Type] the name of the backend tool (if it's supported by us), or the class name of your own solver """ return self.tool if self.tool else self.get_default_tool()
[docs] def get_default_tool(self) -> str: """Get the default tool based on the chosen solving method Returns ------- str the name of the default backend tool """ if self.method: return tool_suggest_table[self.method][0] return tool_suggest_table["optimization"][0]
[docs] def suggest_solving_methods(self): """Prints a list of solving methods to choose from Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.suggest_solving_methods() The following solving methods are supported: {'optimization', 'matrix solvers'} Use `suggest_tools()` to see a full list of backend tools for each method """ print("The following solving methods are supported:") print(solving_methods) print( "\nUse `suggest_tools()` to see a full list of backend tools for each" " method" )
[docs] def suggest_tools(self): """Prints a list of tools based on the solving method chosen Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3, 15 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.suggest_tools() Here's a complete list of inversion tools supported by CoFI (grouped by methods): { "optimization": [ "scipy.optimize.minimize", "scipy.optimize.least_squares" ], "matrix solvers": [ "scipy.linalg.lstsq" ] } >>> inv_options.set_solving_method("matrix solvers") >>> inv_options.suggest_tools() Based on the solving method you've set, the following tools are suggested: ['scipy.linalg.lstsq'] Use `InversionOptions.set_tool(tool_name)` to set a specific tool from above Use `InversionOptions.set_solving_method(method_name)` to change solving method Use `InversionOptions.unset_solving_method()` if you'd like to see more options Check CoFI documentation 'Advanced Usage' section for how to plug in your own solver """ if self.method: tools = tool_suggest_table[self.method] print( "Based on the solving method you've set, the following tools are" " suggested:" ) print(tools) print( "\nUse `InversionOptions.set_tool(tool_name)` to set a specific tool" " from above" ) print( "Use `InversionOptions.set_solving_method(method_name)` to change" " solving method" ) print( "Use `InversionOptions.unset_solving_method()` if you'd like to see" " more options" ) print( "Check CoFI documentation 'Advanced Usage' section for how to plug in" " your own tool or solver" ) else: print( "Here's a complete list of inversion tools supported by CoFI (grouped" " by methods):" ) print(json.dumps(tool_suggest_table, indent=4))
[docs] def suggest_solver_params(self): """Prints required and optional solver-specific parameters Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.suggest_solver_params() Current backend tool scipy.optimize.minimize (default) has the following solver-specific parameters: Required parameters: -- nothing -- Optional parameters & default settings: {'method': None, 'tol': None, 'callback': None, 'options': None} """ tool, dft_suffix = ( (self.tool, "") if self.tool else (f"{self.get_default_tool()}", " (default)") ) solver = tool_dispatch_table[tool] print( f"Current backend tool {tool}{dft_suffix} has the following solver-specific" " parameters:" ) print("Required parameters:") print( solver.required_in_options() if solver.required_in_options() else "-- nothing --" ) print("Optional parameters & default settings:") print( solver.optional_in_options() if solver.optional_in_options() else "-- nothing --" )
[docs] def summary(self): """Helper method that prints a summary of current ``InversionOptions`` object to console Examples -------- .. admonition:: code example :class: dropdown, attention .. code-block:: pycon :emphasize-lines: 3 >>> from cofi import InversionOptions >>> inv_options = InversionOptions() >>> inv_options.summary() ============================= Summary for inversion options ============================= Solving method: None set Use `suggest_solving_methods()` to check available solving methods. ----------------------------- Backend tool: `scipy.optimize.minimize (by default)` - SciPy's optimizers that minimizes a scalar function with respect to one or more variables, check SciPy's documentation page for a list of methods References: ['https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html'] Use `suggest_tools()` to check available backend tools. ----------------------------- Solver-specific parameters: None set Use `suggest_solver_params()` to check required/optional solver-specific parameters. """ self._summary()
def _summary(self, display_lines=True): # inspiration from keras: https://keras.io/examples/vision/mnist_convnet/ title = "Summary for inversion options" display_width = len(title) double_line = "=" * display_width single_line = "-" * display_width if display_lines: print(double_line) print(title) if display_lines: print(double_line) solving_method = self.method if self.method else "None set" tool, dft_suffix = ( (self.tool, "") if self.tool else (f"{self.get_default_tool()}", " (by default)") ) tool = tool_dispatch_table[tool] if isinstance(tool, str) else tool print(f"Solving method: {solving_method}") print("Use `suggest_solving_methods()` to check available solving methods.") if display_lines: print(single_line) print(f"Backend tool: `{tool}{dft_suffix}` - {tool.short_description}") # print(f"Backend tool description: {tool.short_description}") print(f"References: {tool.documentation_links}") print("Use `suggest_tools()` to check available backend tools.") if display_lines: print(single_line) params_suffix = "None set" if len(self.hyper_params) == 0 else "" print(f"Solver-specific parameters: {params_suffix}") for key, val in self.hyper_params.items(): print(f"{key} = {val}") print( "Use `suggest_solver_params()` to check required/optional solver-specific" " parameters." ) def __repr__(self) -> str: class_name = self.__class__.__name__ method = f"'{self.method}'" if self.method else "unknown" tool = ( f"'{self.tool}'" if self.tool else f"(default)'{self.get_default_tool()}'" ) return f"{class_name}(method={method},tool={tool})"