Source code for modelly.viz.solara.model_kwargs

"""Model parameter handling for SolaraViz: validation, splitting and re-instantiation kwargs."""

from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Any

from modelly.scenarios import Scenario
from modelly.viz.solara.params import Slider

if TYPE_CHECKING:
    from modelly.core.model import Model

_NON_KEYWORD_PARAM_KINDS = (
    inspect.Parameter.POSITIONAL_ONLY,
    inspect.Parameter.VAR_POSITIONAL,
)
_IMPLICIT_INIT_PARAMS = ["self", "kwargs", "args", "scenario"]
_ALWAYS_ACCEPTED_PARAMS = ["rng", "seed"]


def _get_scenario_defaults(scenario: Scenario) -> dict[str, Any]:
    """Return defaults for a scenario instance, validating scenario type."""
    scenario_class = type(scenario)
    if not issubclass(scenario_class, Scenario):
        raise ValueError(
            "Expected model.scenario to be a subclass instance of modelly.scenarios.Scenario."
        )

    defaults = getattr(scenario, "_scenario_defaults", {}).copy()
    defaults["rng"] = None

    return defaults


def _split_scenario_params(
    model_parameters: dict[str, Any],
    scenario_defaults: dict[str, Any],
    model_init_params,
) -> tuple[dict[str, Any], dict[str, Any]]:
    """Split parameters into model kwargs and scenario kwargs."""
    kwargs: dict[str, Any] = {}
    scenario_kwargs: dict[str, Any] = {}
    for key, value in model_parameters.items():
        if key in scenario_defaults and key not in model_init_params:
            scenario_kwargs[key] = value
        else:
            kwargs[key] = value
    return kwargs, scenario_kwargs


def _build_model_init_kwargs(
    model: Model,
    model_parameters: dict[str, Any],
    *,
    add_scenario_when_empty: bool,
    require_model_accepts_scenario: bool,
) -> dict[str, Any]:
    """Build kwargs for model re-instantiation, splitting scenario/model params."""
    if not getattr(model, "scenario", None):
        return {**model_parameters}

    scenario = model.scenario
    scenario_class = type(scenario)
    scenario_defaults = _get_scenario_defaults(scenario)
    model_init_params = inspect.signature(type(model).__init__).parameters
    kwargs, scenario_kwargs = _split_scenario_params(
        model_parameters, scenario_defaults, model_init_params
    )

    has_kwargs = any(p.kind == p.VAR_KEYWORD for p in model_init_params.values())
    accepts_scenario = "scenario" in model_init_params or has_kwargs
    should_add_scenario = add_scenario_when_empty or bool(scenario_kwargs)

    if should_add_scenario and (accepts_scenario or not require_model_accepts_scenario):
        kwargs["scenario"] = scenario_class(**scenario_kwargs)
    return kwargs


[docs] def split_model_params(model_params): """Split model parameters into user-adjustable and fixed parameters. Args: model_params: Dictionary of all model parameters Returns: tuple: (user_adjustable_params, fixed_params) """ model_params_input = {} model_params_fixed = {} for k, v in model_params.items(): if check_param_is_fixed(v): model_params_fixed[k] = v else: model_params_input[k] = v return model_params_input, model_params_fixed
[docs] def check_param_is_fixed(param): """Check if a parameter is fixed (not user-adjustable). Args: param: Parameter to check Returns: bool: True if parameter is fixed, False otherwise """ if isinstance(param, Slider): return False if not isinstance(param, dict): return True if "type" not in param: return True
def _resolve_init_signature(model_or_func): """Return ``(model, init parameters)`` for a model instance or an init function.""" if inspect.isfunction(model_or_func) or inspect.ismethod(model_or_func): init_func = model_or_func model = None else: model = model_or_func init_func = type(model).__init__ return model, inspect.signature(init_func).parameters def _require_keyword_params(model_parameters) -> None: """Raise when the init function takes positional-only or ``*args`` parameters.""" has_non_keyword_param = any( param.kind in _NON_KEYWORD_PARAM_KINDS for param in model_parameters.values() ) if has_non_keyword_param: raise ValueError( "Modelly's visualization requires the use of keyword arguments to ensure the parameters are passed to Solara correctly. Please ensure all model parameters are of form param=value" ) def _require_known_params( model_parameters, model_params, scenario_defaults, has_var_keyword ) -> None: """Raise on missing required parameters or on parameters the model does not take.""" for name in model_parameters: if ( model_parameters[name].default == inspect.Parameter.empty and name not in model_params and name not in _IMPLICIT_INIT_PARAMS and not has_var_keyword ): raise ValueError(f"Missing required model parameter: {name}") for name in model_params: if ( name not in model_parameters and name not in scenario_defaults and name not in _ALWAYS_ACCEPTED_PARAMS and not has_var_keyword ): raise ValueError(f"Invalid model parameter: {name}") def _check_model_params(model_or_func, model_params): """Check if model parameters are valid for the model's initialization function. Args: model_or_func: Model instance or its initialization function model_params: Dictionary of model parameters Raises: ValueError: If a parameter is not valid for the model's initialization function """ model, model_parameters = _resolve_init_signature(model_or_func) _require_keyword_params(model_parameters) has_var_keyword = any( param.kind == inspect.Parameter.VAR_KEYWORD for param in model_parameters.values() ) scenario_defaults = {} if model is not None and getattr(model, "scenario", None): scenario_defaults = _get_scenario_defaults(model.scenario) _require_known_params( model_parameters, model_params, scenario_defaults, has_var_keyword )