def check_true_int_param( index: int, elems: Sequence[Any], param: str, func: str, ) -> int: """ Parse an integer parameter that isn't allowed to be a distribution. """ try: value = elems[index] except IndexError: raise DistParseError( f"Missing parameter <{param}> for function '{func}'. " f"You provided {elems}.") if isinstance(value, float): raise DistParseError( f"Parameter <{param}> value '{value}' in function '{func}' " "cannot be a float.") try: return int(value) except (ValueError, TypeError): raise DistParseError( f"Could not parse parameter <{param}> value '{elems[index]}' " f"as an integer in function '{func}'.")
def check_str_param(index: int, elems: Sequence[Any], name: str, func: str) -> Distribution[str]: """ Does error handling and conversion of str parameters. """ try: param = parse_str_dist(elems[index]) except IndexError: raise DistParseError( f"Missing parameter <{name}> for function '{func}'. " f"You provided {elems}.") except ValueError: raise DistParseError( f"Could not parse parameter <{name}> value '{elems[index]}' " f"as a categorical distribution for function '{func}'.") return param
def parse_neg_binomial(elems: Sequence[Any]) -> dist.NegBinomial: """ Parse a list definition as a negative binomial distribution definition. Examples: >>> r = parse_neg_binomial([5, 0.4, 2]) >>> r.n IntConst(5) >>> r.p FloatConst(0.4) >>> r.loc IntConst(2) """ str_func = "nbinomial" check_n_params(["n", "p", "loc"], elems, str_func) n = check_int_param(0, elems, "n", str_func) p = check_float_param(1, elems, "p", str_func) if not isinstance(p, dist.PDF): raise DistParseError( "Parameter <p> to nbinomial must be a float between 0 and 1. " "You provided '{elems[1]}'.") loc = check_int_param(2, elems, "loc", str_func) return dist.NegBinomial(n, p, loc)
def parse_string_as_int(elem: Union[int, str]) -> Distribution[int]: try: parsed_elem: Distribution[int] = dist.IntConst(int(elem)) except ValueError: raise DistParseError(f"Could not parse value '{elem}' as an integer.") return parsed_elem
def parse_int_list(elems: Sequence[Any]) -> AOList[int]: """ Parse a list of integer distributions. Examples: >>> parse_int_list([1, 2, 3]) AOList([IntConst(1), IntConst(2), IntConst(3)]) >>> parse_int_list(["sample", 5, ["range", 1, 10]]) Sample(5, Range(IntConst(1), IntConst(10))) >>> parse_int_list(["monotonic", 5, ["range", 1, 10]]) Monotonic(5, Range(IntConst(1), IntConst(10))) >>> parse_int_list(["seq", 5, ["range", 1, 10]]) Seq(5, Range(IntConst(1), IntConst(10))) """ if len(elems) > 0 and isinstance(elems[0], str): func = int_list_fn_finder(elems[0]) if func is not None: return func(elems[1:]) try: return AOList([parse_int_dist(e) for e in elems]) except DistParseError as e: raise DistParseError( f"Error while parsing integer list {repr(elems)}. {e.msg}")
def check_true_float_param(index: int, elems: Sequence[Any], param: str, func: str) -> float: """ Parse a float parameter that isn't allowed to be a distribution. """ try: value = elems[index] except IndexError: raise DistParseError( f"Missing parameter <{param}> for function '{func}'. " f"You provided {elems}.") try: return float(value) except (ValueError, TypeError): raise DistParseError( f"Could not parse parameter <{param}> value '{elems[index]}' " f"as a float in function '{func}'.")
def check_n_params(valid: Sequence[str], elems: Sequence[Any], func: str) -> None: """ Checks if there are more parameters that was expected. """ if len(elems) != len(valid): raise DistParseError( f"Function '{func}' expects {len(valid)} parameters: {valid}. " f"Received {len(elems)} parameters: {elems}.") return None
def check_float_param( index: int, elems: Sequence[Any], name: str, func: str, ) -> DistributionIF: """ Does error handling and conversion of float parameters. """ try: param = parse_float_dist(elems[index]) except IndexError: raise DistParseError( f"Missing parameter <{name}> for function '{func}'. " f"You provided {elems}.") except ValueError: raise DistParseError( f"Could not parse parameter <{name}> value '{elems[index]}' " f"as a continuous or discrete distribution for function '{func}'.") return param
def parse_str_dist(elem: Any) -> Distribution[str]: """ Parses lists or literals to a categorical distribution. """ if isinstance(elem, (int, float, str)): parsed_elem: Distribution[str] = dist.StrConst(str(elem)) elif isinstance(elem, list): parsed_elem = parse_list_in_dist(elem, str_dist_fn_finder) elif isinstance(elem, Distribution): parsed_elem = elem else: raise DistParseError("Recieved invalid input: {repr(elem)}.") if isinstance(parsed_elem, dist.Categorical): return parsed_elem else: raise DistParseError( f"Could not parse value '{elem}' as a categorical distribution.")
def parse_int_dist(elem: Any) -> Distribution[int]: """ Parse an integer distribution specification. Examples: >>> parse_float_dist(['range', 1, 2]) Range(IntConst(1), IntConst(2)) >>> parse_int_dist(1) IntConst(1) >>> parse_int_dist(['choose', [3, 5, 7]]) ChooseI([IntConst(3), IntConst(5), IntConst(7)]) >>> parse_int_dist([1, 2, 3]) Traceback (most recent call last): ... DistParseError: Received a list but cannot take a list here: {...}. """ if isinstance(elem, (int, str)): parsed_elem = parse_string_as_int(elem) elif isinstance(elem, list): parsed_elem = parse_list_in_dist(elem, int_dist_fn_finder) elif isinstance(elem, Distribution): parsed_elem = elem elif isinstance(elem, float): raise DistParseError( "This distribution/parameter doesn't support float values. " "Please convert '{elem}' to an integer.") else: raise DistParseError("Recieved invalid input: {repr(elem)}.") if isinstance(parsed_elem, (dist.PMF, dist.Trunc)): return parsed_elem else: raise DistParseError( f"Could not parse '{elem}' as a discrete distribution.")
def parse_float_dist(elem: Any) -> DistributionIF: """ Parse a float distribution specification. Examples: >>> parse_float_dist(['uniform', 1, 2]) Uniform(FloatConst(1.0), FloatConst(2.0)) >>> parse_float_dist(['uniform', 1, ['uniform', 3, 4.5]]) Uniform(FloatConst(1.0), Uniform(FloatConst(3.0), FloatConst(4.5))) >>> parse_float_dist(['range', 1, 3]) Range(IntConst(1), IntConst(3)) >>> parse_float_dist(['uniform', 1, ['range', 3, 7]]) Uniform(FloatConst(1.0), Range(IntConst(3), IntConst(7))) >>> parse_float_dist(['choose', [3, 5, 7]]) ChooseF([FloatConst(3.0), FloatConst(5.0), FloatConst(7.0)]) >>> parse_float_dist([1, 2, 3]) Traceback (most recent call last): ... DistParseError: Received a list but cannot take a list here: {...}. """ if isinstance(elem, (int, float, str)): parsed_elem = parse_string_as_float(elem) elif isinstance(elem, list): parsed_elem = parse_list_in_dist(elem, float_dist_fn_finder) elif isinstance(elem, Distribution): parsed_elem = elem else: raise DistParseError("Recieved invalid input: {repr(elem)}.") if isinstance(parsed_elem, (dist.PDF, dist.PMF, dist.Trunc)): return parsed_elem else: raise DistParseError(f"Could not parse '{elem}' as a continuous or " "discrete distribution.")
def parse_list_in_dist( elems: Sequence[Any], fn_finder: Callable[[str], Optional[Callable[[Sequence[Any]], T]]], ) -> T: """ Parse a list while parsing a distribution. This could be a function or an actual list. """ if len(elems) == 0: raise DistParseError("Encountered an empty list.") elif not isinstance(elems[0], str): raise DistParseError( f"Received a list but cannot take a list here: {elems}") func = fn_finder(elems[0]) if func is None: raise DistParseError(f"Recieved a list or invalid function: {elems}.") else: parsed_elem = func(elems[1:]) return parsed_elem
def parse_str_list(elems: Sequence[Any]) -> AOList[str]: """ Parse a list of string distributions. >>> parse_str_list(["sample", 5, ["choose", ["one", "two", "three"]]]) Sample(5, ChooseS([StrConst('one'), StrConst('two'), StrConst('three')])) >>> parse_str_list(["sample", 5, ["one", "two", "three"]]) Traceback (most recent call last): ... DistParseError: Received a list or invalid function ... """ if len(elems) > 0 and isinstance(elems[0], str): func = str_list_fn_finder(elems[0]) if func is not None: return func(elems[1:]) try: return AOList([parse_str_dist(e) for e in elems]) except DistParseError as e: raise DistParseError( f"Error while parsing str list {repr(elems)}. {e.msg}")
def parse_float_list(elems: Sequence[Any]) -> AOList[Union[int, float]]: """ Parse a list of float distributions. Examples: >>> parse_float_list([1, 2, 3]) AOList([FloatConst(1.0), FloatConst(2.0), FloatConst(3.0)]) >>> parse_float_list([2, ['uniform', 3, 4.6]]) AOList([FloatConst(2.0), Uniform(FloatConst(3.0), FloatConst(4.6))]) >>> parse_float_list(["sample", 5, ["chi", 1, 1, 1]]) Sample(5, Chi(FloatConst(1.0), FloatConst(1.0), FloatConst(1.0))) >>> parse_float_list(["monotonic", 5, ["chi", 1, 1, 1]]) Monotonic(5, Chi(FloatConst(1.0), FloatConst(1.0), FloatConst(1.0))) >>> parse_float_list(["seq", 5, ["chi", 1, 1, 1]]) Seq(5, Chi(FloatConst(1.0), FloatConst(1.0), FloatConst(1.0))) """ if len(elems) > 0 and isinstance(elems[0], str): func = float_list_fn_finder(elems[0]) if func is not None: return func(elems[1:]) try: # I'm being a bit cheeky with type hints here. # Return from parse_float_dist is DistributionIF # The type system isn't expressive enough to destructure to # AOList[Union[int, float]]. comp: Sequence[Distribution] = [parse_float_dist(e) for e in elems] return AOList(comp) except DistParseError as e: raise DistParseError( f"Error while parsing float list {repr(elems)}. {e.msg}")
def parse_string_as_float(elem: Union[int, float, str]) -> DistributionIF: try: parsed_elem: DistributionIF = dist.FloatConst(float(elem)) except ValueError: raise DistParseError(f"Could not parse value '{elem}' as a float.") return parsed_elem