Source code for httpstan.services.arguments
"""Lookup arguments and argument default values for stan::services functions."""
import enum
import functools
import importlib.resources
import json
import re
import time
import types
import typing
Method = enum.Enum("Method", "SAMPLE OPTIMIZE VARIATIONAL DIAGNOSE")
DEFAULTS_LOOKUP = None # lazy loaded by lookup_default
[docs]def _pythonize_cmdstan_type(type_name: str) -> type:
"""Turn CmdStan C++ type name into Python type.
For example, "double" becomes ``float`` (the type).
"""
if type_name == "double":
return float
if type_name in {"int", "unsigned int"}:
return int
if type_name.startswith("bool"):
return bool
if type_name == "list element":
raise NotImplementedError(f"Cannot convert CmdStan `{type_name}` to Python type.")
if type_name == "string":
return str
raise ValueError(f"Cannot convert CmdStan `{type_name}` to Python type.")
[docs]@functools.lru_cache()
def lookup_default(method: Method, arg: str) -> typing.Union[float, int]:
"""Fetch default for named argument in a stan:services `function`.
Uses defaults from CmdStan. The file ``cmdstan-help-all.json`` is generated
with the script ``scripts/parse_cmdstan_help.py`` from the output of running
a CmdStan binary with the argument ``help-all`` (e.g., ``
examples/bernoulli/bernoulli help-all``)
"""
global DEFAULTS_LOOKUP
if DEFAULTS_LOOKUP is None:
DEFAULTS_LOOKUP = json.loads(importlib.resources.read_text(__package__, "cmdstan-help-all.json"))
# special handling for random_seed, argument name differs from CmdStan name
if arg == "random_seed":
# CmdStan generates an unsigned integer using boost::posix_time (line 80 of command.hpp)
return int(time.time())
# special handling for chain, argument name differs from CmdStan name
if arg == "chain":
return 1
# special handling for ``num_thin``, since argument name differs from CmdStan name
if arg == "num_thin":
arg = "thin"
# special handling for ``refresh`` since the choice is up to httpstan, value
# determines how often messages are sent to callback logger
if arg == "refresh":
return 100
# special handling for init_radius. There is an interaction with 'init'.
if arg == "init_radius":
return 2
defaults_for_method = DEFAULTS_LOOKUP["method"][method.name.lower()]
try:
item = next(filter(lambda item: item["name"] == arg, defaults_for_method))
except StopIteration:
raise ValueError(f"No argument `{arg}` is associated with `{method}`.")
python_type = _pythonize_cmdstan_type(item["type"])
if python_type == bool:
# bool needs special handling because bool("0") == True
return int(item["default"] != "0")
assert python_type in {int, float}
return typing.cast(typing.Union[float, int], python_type(item["default"]))
[docs]def function_arguments(function_name: str, services_module: types.ModuleType) -> typing.List[str]:
"""Get function arguments for stan::services `function_name`.
This function parses a function's docstring to get argument names. This is
an inferior method to using `inspect.Signature.from_callable(function)`.
Unfortunately, pybind11 does not support this use of `inspect`.
A compiled `services_module` is required for the lookup. Only simple function
arguments are returned. For example, callback writers and var_context
arguments are dropped.
Arguments:
function_name: Name of the function.
services_module (module): Compiled model-specific services extension module.
Returns:
Argument names for `function_name`.
"""
function = getattr(services_module, f"{function_name}_wrapper")
docstring = function.__doc__
# first line look something like this: function_name(arg1: int, arg2: int, ...) -> int
function_name_with_arguments = docstring.split(" -> ", 1).pop(0)
parameters = re.findall(r"(\w+): \w+", function_name_with_arguments)
# remove arguments which are specific to the wrapper
arguments_exclude = {"socket_filename"}
return list(filter(lambda arg: arg not in arguments_exclude, parameters))