Skip to content

brm

Model fitting wrappers.

This module contains the brms::brm() wrapper used by brmspy.brms.fit() / brmspy.brms.brm().

Notes

This code executes inside the worker process (the process that hosts the embedded R session).

Attributes

FitResult = IDResult[IDBrm] module-attribute

ProxyListSexpVector = Union[SexpWrapper, ListSexpVector, None] module-attribute

Classes

SexpWrapper dataclass

Lightweight handle for an R object stored in the worker.

The worker keeps the real rpy2 Sexp in an internal cache and replaces it in results with this wrapper. When passed back to the worker, the wrapper is resolved to the original Sexp again.

Notes
  • SexpWrapper instances are only meaningful within the lifetime of the worker process that produced them. After a worker restart, previously returned wrappers can no longer be reattached.
  • This type exists to keep the main process free of rpy2 / embedded-R state.
Source code in brmspy/types/session.py
@dataclass
class SexpWrapper:
    """
    Lightweight handle for an R object stored in the worker.

    The worker keeps the real rpy2 `Sexp` in an internal cache and replaces it in
    results with this wrapper. When passed back to the worker, the wrapper is
    resolved to the original `Sexp` again.

    Notes
    -----
    - `SexpWrapper` instances are only meaningful within the lifetime of the
      worker process that produced them. After a worker restart, previously
      returned wrappers can no longer be reattached.
    - This type exists to keep the main process free of rpy2 / embedded-R state.
    """

    _rid: int
    _repr: str

    def __str__(self) -> str:
        return self._repr

    def __repr__(self) -> str:
        return self._repr

Attributes

_rid instance-attribute
_repr instance-attribute

Functions

__str__()
Source code in brmspy/types/session.py
def __str__(self) -> str:
    return self._repr
__repr__()
Source code in brmspy/types/session.py
def __repr__(self) -> str:
    return self._repr
__init__(_rid, _repr)

IDBrm

Bases: IDConstantData

Typed arviz.InferenceData for fitted brms models.

Extends arviz.InferenceData with type hints for IDE autocomplete. In brmspy, the fitted model result typically exposes an .idata attribute of this type.

Attributes:

Name Type Description
posterior Dataset

Posterior samples of model parameters.

posterior_predictive Dataset

Posterior predictive samples (with observation noise).

log_likelihood Dataset

Log-likelihood values for each observation.

observed_data Dataset

Original observed response data.

coords dict

Coordinate mappings for dimensions (inherited from arviz.InferenceData).

dims dict

Dimension specifications for variables (inherited from arviz.InferenceData).

See Also

brmspy.brms.brm : Creates fitted model results (alias: brmspy.brms.fit). arviz.InferenceData : Base class documentation.

Examples:

from brmspy import brms

model = brms.brm("y ~ x", data=df, chains=4)

# Type checking and autocomplete
assert isinstance(model.idata, IDFit)
print(model.idata.posterior)
Source code in brmspy/types/brms_results.py
class IDBrm(IDConstantData):
    """
    Typed `arviz.InferenceData` for fitted brms models.

    Extends `arviz.InferenceData` with type hints for IDE autocomplete. In brmspy,
    the fitted model result typically exposes an `.idata` attribute of this type.

    Attributes
    ----------
    posterior : xr.Dataset
        Posterior samples of model parameters.
    posterior_predictive : xr.Dataset
        Posterior predictive samples (with observation noise).
    log_likelihood : xr.Dataset
        Log-likelihood values for each observation.
    observed_data : xr.Dataset
        Original observed response data.
    coords : dict
        Coordinate mappings for dimensions (inherited from `arviz.InferenceData`).
    dims : dict
        Dimension specifications for variables (inherited from `arviz.InferenceData`).

    See Also
    --------
    brmspy.brms.brm : Creates fitted model results (alias: `brmspy.brms.fit`).
    arviz.InferenceData : Base class documentation.

    Examples
    --------
    ```python
    from brmspy import brms

    model = brms.brm("y ~ x", data=df, chains=4)

    # Type checking and autocomplete
    assert isinstance(model.idata, IDFit)
    print(model.idata.posterior)
    ```
    """

    posterior: xr.Dataset
    posterior_predictive: xr.Dataset
    log_likelihood: xr.Dataset
    observed_data: xr.Dataset
    coords: xr.Dataset
    dims: xr.Dataset

Attributes

posterior instance-attribute
posterior_predictive instance-attribute
log_likelihood instance-attribute
observed_data instance-attribute
coords instance-attribute
dims instance-attribute

PriorSpec dataclass

Python representation of a brms prior specification.

This dataclass provides a typed interface to brms::prior_string() arguments, allowing Python developers to specify priors with IDE autocomplete and type checking. Use the prior() factory function to create instances.

Attributes:

Name Type Description
prior str

Prior distribution as string (e.g., "normal(0, 1)", "exponential(2)").

class_ (str, optional)

Parameter class: "b" (fixed effects), "sd" (group SD), "Intercept", "sigma", "cor", etc.

coef (str, optional)

Specific coefficient name for class-level priors.

group (str, optional)

Grouping variable for hierarchical effects.

dpar (str, optional)

Distributional parameter (e.g., "sigma", "phi", "zi").

resp (str, optional)

Response variable for multivariate models.

nlpar (str, optional)

Non-linear parameter name.

lb (float, optional)

Lower bound for truncated priors.

ub (float, optional)

Upper bound for truncated priors.

See Also

prior : Factory function to create PriorSpec instances. brms::prior_string : R documentation

Examples:

Create prior specifications (prefer using prior()):

from brmspy.types import PriorSpec

# Fixed effect prior
p1 = PriorSpec(prior="normal(0, 1)", class_="b")

# Group-level SD prior
p2 = PriorSpec(prior="exponential(2)", class_="sd", group="patient")

# Coefficient-specific prior with bounds
p3 = PriorSpec(prior="normal(0, 1)", class_="b", coef="age", lb=0)
Source code in brmspy/types/brms_results.py
@dataclass(frozen=True)
class PriorSpec:
    """
    Python representation of a brms prior specification.

    This dataclass provides a typed interface to `brms::prior_string()` arguments,
    allowing Python developers to specify priors with IDE autocomplete and type
    checking. Use the [`prior()`][brmspy.brms.prior] factory function to create
    instances.

    Attributes
    ----------
    prior : str
        Prior distribution as string (e.g., ``"normal(0, 1)"``, ``"exponential(2)"``).
    class_ : str, optional
        Parameter class: ``"b"`` (fixed effects), ``"sd"`` (group SD),
        ``"Intercept"``, ``"sigma"``, ``"cor"``, etc.
    coef : str, optional
        Specific coefficient name for class-level priors.
    group : str, optional
        Grouping variable for hierarchical effects.
    dpar : str, optional
        Distributional parameter (e.g., ``"sigma"``, ``"phi"``, ``"zi"``).
    resp : str, optional
        Response variable for multivariate models.
    nlpar : str, optional
        Non-linear parameter name.
    lb : float, optional
        Lower bound for truncated priors.
    ub : float, optional
        Upper bound for truncated priors.

    See Also
    --------
    prior : Factory function to create `PriorSpec` instances.
    brms::prior_string : [R documentation](https://paulbuerkner.com/brms/reference/prior_string.html)

    Examples
    --------
    Create prior specifications (prefer using [`prior()`][brmspy.brms.prior]):

    ```python
    from brmspy.types import PriorSpec

    # Fixed effect prior
    p1 = PriorSpec(prior="normal(0, 1)", class_="b")

    # Group-level SD prior
    p2 = PriorSpec(prior="exponential(2)", class_="sd", group="patient")

    # Coefficient-specific prior with bounds
    p3 = PriorSpec(prior="normal(0, 1)", class_="b", coef="age", lb=0)
    ```
    """

    prior: str
    class_: str | None = None
    coef: str | None = None
    group: str | None = None
    dpar: str | None = None
    resp: str | None = None
    nlpar: str | None = None
    lb: float | None = None
    ub: float | None = None

    def to_brms_kwargs(self) -> dict[str, Any]:
        """
        Convert PriorSpec to keyword arguments for brms::prior_string().

        Maps Python dataclass fields to R function arguments, handling
        the `class_` -> `class` parameter name conversion.

        Returns
        -------
        dict
            Keyword arguments ready for brms::prior_string()

        Examples
        --------
        ```python
        from brmspy import prior
        p = prior("normal(0, 1)", class_="b", coef="age")
        kwargs = p.to_brms_kwargs()
        print(kwargs)
        # {'prior': 'normal(0, 1)', 'class': 'b', 'coef': 'age'}
        ```
        """
        out: dict[str, Any] = {"prior": self.prior}
        if self.class_ is not None:
            out["class"] = self.class_
        if self.coef is not None:
            out["coef"] = self.coef
        if self.group is not None:
            out["group"] = self.group
        if self.dpar is not None:
            out["dpar"] = self.dpar
        if self.resp is not None:
            out["resp"] = self.resp
        if self.nlpar is not None:
            out["nlpar"] = self.nlpar
        if self.lb is not None:
            out["lb"] = self.lb
        if self.ub is not None:
            out["ub"] = self.ub
        return out

Attributes

prior instance-attribute
class_ = None class-attribute instance-attribute
coef = None class-attribute instance-attribute
group = None class-attribute instance-attribute
dpar = None class-attribute instance-attribute
resp = None class-attribute instance-attribute
nlpar = None class-attribute instance-attribute
lb = None class-attribute instance-attribute
ub = None class-attribute instance-attribute

Functions

to_brms_kwargs()

Convert PriorSpec to keyword arguments for brms::prior_string().

Maps Python dataclass fields to R function arguments, handling the class_ -> class parameter name conversion.

Returns:

Type Description
dict

Keyword arguments ready for brms::prior_string()

Examples:

from brmspy import prior
p = prior("normal(0, 1)", class_="b", coef="age")
kwargs = p.to_brms_kwargs()
print(kwargs)
# {'prior': 'normal(0, 1)', 'class': 'b', 'coef': 'age'}
Source code in brmspy/types/brms_results.py
def to_brms_kwargs(self) -> dict[str, Any]:
    """
    Convert PriorSpec to keyword arguments for brms::prior_string().

    Maps Python dataclass fields to R function arguments, handling
    the `class_` -> `class` parameter name conversion.

    Returns
    -------
    dict
        Keyword arguments ready for brms::prior_string()

    Examples
    --------
    ```python
    from brmspy import prior
    p = prior("normal(0, 1)", class_="b", coef="age")
    kwargs = p.to_brms_kwargs()
    print(kwargs)
    # {'prior': 'normal(0, 1)', 'class': 'b', 'coef': 'age'}
    ```
    """
    out: dict[str, Any] = {"prior": self.prior}
    if self.class_ is not None:
        out["class"] = self.class_
    if self.coef is not None:
        out["coef"] = self.coef
    if self.group is not None:
        out["group"] = self.group
    if self.dpar is not None:
        out["dpar"] = self.dpar
    if self.resp is not None:
        out["resp"] = self.resp
    if self.nlpar is not None:
        out["nlpar"] = self.nlpar
    if self.lb is not None:
        out["lb"] = self.lb
    if self.ub is not None:
        out["ub"] = self.ub
    return out
__init__(prior, class_=None, coef=None, group=None, dpar=None, resp=None, nlpar=None, lb=None, ub=None)

FormulaConstruct dataclass

A composite formula expression built from parts.

FormulaConstruct stores a tree of nodes (FormulaPart and/or R objects) representing expressions combined with +. It is primarily created by calling the public formula helpers exposed by brmspy.brms.

Notes

The + operator supports grouping:

  • a + b + c becomes a single summand (one “group”)
  • (a + b) + (a + b) becomes two summands (two “groups”)

Use iter_summands() to iterate over these groups in a deterministic way.

Source code in brmspy/types/formula_dsl.py
@dataclass
class FormulaConstruct:
    """
    A composite formula expression built from parts.

    `FormulaConstruct` stores a tree of nodes (`FormulaPart` and/or R objects)
    representing expressions combined with `+`. It is primarily created by
    calling the public formula helpers exposed by [`brmspy.brms`][brmspy.brms].

    Notes
    -----
    The `+` operator supports grouping:

    - `a + b + c` becomes a single summand (one “group”)
    - `(a + b) + (a + b)` becomes two summands (two “groups”)

    Use [`iter_summands()`][brmspy.types.formula_dsl.FormulaConstruct.iter_summands]
    to iterate over these groups in a deterministic way.
    """

    _parts: list[Node]

    @classmethod
    def _formula_parse(cls, obj: Other) -> "FormulaConstruct":
        """
        Convert a supported value into a `FormulaConstruct`.

        Parameters
        ----------
        obj
            One of: `FormulaConstruct`, `FormulaPart`, string (interpreted as `bf(<string>)`),
            or `ProxyListSexpVector`.

        Returns
        -------
        FormulaConstruct
        """
        if isinstance(obj, FormulaConstruct):
            return obj
        if isinstance(obj, ProxyListSexpVector):
            return FormulaConstruct(_parts=[obj])
        if isinstance(obj, FormulaPart):
            return FormulaConstruct(_parts=[obj])
        if isinstance(obj, str):
            part = FormulaPart(_fun="bf", _args=[obj], _kwargs={})
            return FormulaConstruct(_parts=[part])
        raise TypeError(
            f"Cannot parse object of type {type(obj)!r} into FormulaConstruct"
        )

    def __add__(self, other: Other):
        """
        Combine two formula expressions with `+`.

        Parameters
        ----------
        other
            Value to add. Strings are treated as `bf(<string>)`.

        Returns
        -------
        FormulaConstruct
            New combined expression.
        """
        if isinstance(other, (FormulaPart, str, ProxyListSexpVector)):
            other = FormulaConstruct._formula_parse(other)

        if not isinstance(other, FormulaConstruct):
            raise ArithmeticError(
                "When adding values to formula, they must be FormulaConstruct or parseable to FormulaConstruct"
            )

        if len(other._parts) <= 1:
            return FormulaConstruct(_parts=self._parts + other._parts)
        else:
            return FormulaConstruct(_parts=[self._parts, other._parts])

    def __radd__(self, other: Other) -> "FormulaConstruct":
        """Support `"y ~ x" + bf("z ~ 1")` by coercing the left operand."""
        return self._formula_parse(other) + self

    def iter_summands(self) -> Iterator[Summand]:
        """
        Iterate over arithmetic groups (summands).

        Returns
        -------
        Iterator[tuple[FormulaPart | ProxyListSexpVector, ...]]
            Each yielded tuple represents one summand/group.

        Examples
        --------
        ```python
        from brmspy.brms import bf, gaussian, set_rescor

        f = bf("y ~ x") + gaussian() + set_rescor(True)
        for summand in f.iter_summands():
            print(summand)
        ```
        """

        def _groups(node: Node) -> Iterator[list[FormulaPart | ProxyListSexpVector]]:
            # Leaf node: single bf/family/etc
            if isinstance(node, (FormulaPart, ProxyListSexpVector)):
                return ([node],)  # one group with one element

            if isinstance(node, list):
                # If any child is a list, this node represents a "+"
                # between sub-expressions, so recurse into each child.
                if any(isinstance(child, list) for child in node):
                    for child in node:
                        yield from _groups(child)
                else:
                    # All children are leaves -> one summand
                    out: list[FormulaPart | ProxyListSexpVector] = []
                    for child in node:
                        if isinstance(child, (FormulaPart, ProxyListSexpVector, Sexp)):
                            child = cast(FormulaPart | ProxyListSexpVector, child)
                            out.append(child)
                        else:
                            raise TypeError(
                                f"Unexpected leaf node type in FormulaConstruct: {type(child)!r}"
                            )
                    yield out
                return

            raise TypeError(f"Unexpected node type in FormulaConstruct: {type(node)!r}")

        # self._parts is always a list[Node]
        for group in _groups(self._parts):
            yield tuple(group)

    # Make __iter__ return summands by default
    def __iter__(self) -> Iterator[Summand]:
        """Alias for [`iter_summands()`][brmspy.types.formula_dsl.FormulaConstruct.iter_summands]."""
        return self.iter_summands()

    def iterate(self) -> Iterator[FormulaPart | ProxyListSexpVector]:
        """
        Iterate over all leaf nodes in left-to-right order.

        This flattens the expression tree, unlike
        [`iter_summands()`][brmspy.types.formula_dsl.FormulaConstruct.iter_summands], which
        respects grouping.

        Returns
        -------
        Iterator[FormulaPart | ProxyListSexpVector]
        """

        def _walk(node: Node) -> Iterator[FormulaPart | ProxyListSexpVector]:
            if isinstance(node, FormulaPart):
                yield node
            elif isinstance(node, ProxyListSexpVector):
                yield node
            elif isinstance(node, list):
                for child in node:
                    yield from _walk(child)
            else:
                raise TypeError(
                    f"Unexpected node type in FormulaConstruct: {type(node)!r}"
                )

        for root in self._parts:
            yield from _walk(root)

    def __str__(self) -> str:
        return self._pretty(self._parts)

    def _pretty(self, node, _outer=True) -> str:
        if isinstance(node, FormulaPart):
            return str(node)

        if isinstance(node, (ProxyListSexpVector, Sexp)):
            return _sexp_to_str(node)

        if isinstance(node, list):
            # Pretty-print each child
            rendered = [self._pretty(child, _outer=False) for child in node]

            # If only one child, no parentheses needed
            if len(rendered) == 1:
                return rendered[0]

            # Multiple children → join with " + "
            inner = " + ".join(rendered)
            if _outer:
                return inner
            else:
                return f"({inner})"

        raise TypeError(f"Unexpected node type {type(node)!r} in pretty-printer")

    def __repr__(self) -> str:
        return self.__str__()

Attributes

_parts instance-attribute

Functions

_formula_parse(obj) classmethod

Convert a supported value into a FormulaConstruct.

Parameters:

Name Type Description Default
obj Other

One of: FormulaConstruct, FormulaPart, string (interpreted as bf(<string>)), or ProxyListSexpVector.

required

Returns:

Type Description
FormulaConstruct
Source code in brmspy/types/formula_dsl.py
@classmethod
def _formula_parse(cls, obj: Other) -> "FormulaConstruct":
    """
    Convert a supported value into a `FormulaConstruct`.

    Parameters
    ----------
    obj
        One of: `FormulaConstruct`, `FormulaPart`, string (interpreted as `bf(<string>)`),
        or `ProxyListSexpVector`.

    Returns
    -------
    FormulaConstruct
    """
    if isinstance(obj, FormulaConstruct):
        return obj
    if isinstance(obj, ProxyListSexpVector):
        return FormulaConstruct(_parts=[obj])
    if isinstance(obj, FormulaPart):
        return FormulaConstruct(_parts=[obj])
    if isinstance(obj, str):
        part = FormulaPart(_fun="bf", _args=[obj], _kwargs={})
        return FormulaConstruct(_parts=[part])
    raise TypeError(
        f"Cannot parse object of type {type(obj)!r} into FormulaConstruct"
    )
__add__(other)

Combine two formula expressions with +.

Parameters:

Name Type Description Default
other Other

Value to add. Strings are treated as bf(<string>).

required

Returns:

Type Description
FormulaConstruct

New combined expression.

Source code in brmspy/types/formula_dsl.py
def __add__(self, other: Other):
    """
    Combine two formula expressions with `+`.

    Parameters
    ----------
    other
        Value to add. Strings are treated as `bf(<string>)`.

    Returns
    -------
    FormulaConstruct
        New combined expression.
    """
    if isinstance(other, (FormulaPart, str, ProxyListSexpVector)):
        other = FormulaConstruct._formula_parse(other)

    if not isinstance(other, FormulaConstruct):
        raise ArithmeticError(
            "When adding values to formula, they must be FormulaConstruct or parseable to FormulaConstruct"
        )

    if len(other._parts) <= 1:
        return FormulaConstruct(_parts=self._parts + other._parts)
    else:
        return FormulaConstruct(_parts=[self._parts, other._parts])
__radd__(other)

Support "y ~ x" + bf("z ~ 1") by coercing the left operand.

Source code in brmspy/types/formula_dsl.py
def __radd__(self, other: Other) -> "FormulaConstruct":
    """Support `"y ~ x" + bf("z ~ 1")` by coercing the left operand."""
    return self._formula_parse(other) + self
iter_summands()

Iterate over arithmetic groups (summands).

Returns:

Type Description
Iterator[tuple[FormulaPart | ProxyListSexpVector, ...]]

Each yielded tuple represents one summand/group.

Examples:

from brmspy.brms import bf, gaussian, set_rescor

f = bf("y ~ x") + gaussian() + set_rescor(True)
for summand in f.iter_summands():
    print(summand)
Source code in brmspy/types/formula_dsl.py
def iter_summands(self) -> Iterator[Summand]:
    """
    Iterate over arithmetic groups (summands).

    Returns
    -------
    Iterator[tuple[FormulaPart | ProxyListSexpVector, ...]]
        Each yielded tuple represents one summand/group.

    Examples
    --------
    ```python
    from brmspy.brms import bf, gaussian, set_rescor

    f = bf("y ~ x") + gaussian() + set_rescor(True)
    for summand in f.iter_summands():
        print(summand)
    ```
    """

    def _groups(node: Node) -> Iterator[list[FormulaPart | ProxyListSexpVector]]:
        # Leaf node: single bf/family/etc
        if isinstance(node, (FormulaPart, ProxyListSexpVector)):
            return ([node],)  # one group with one element

        if isinstance(node, list):
            # If any child is a list, this node represents a "+"
            # between sub-expressions, so recurse into each child.
            if any(isinstance(child, list) for child in node):
                for child in node:
                    yield from _groups(child)
            else:
                # All children are leaves -> one summand
                out: list[FormulaPart | ProxyListSexpVector] = []
                for child in node:
                    if isinstance(child, (FormulaPart, ProxyListSexpVector, Sexp)):
                        child = cast(FormulaPart | ProxyListSexpVector, child)
                        out.append(child)
                    else:
                        raise TypeError(
                            f"Unexpected leaf node type in FormulaConstruct: {type(child)!r}"
                        )
                yield out
            return

        raise TypeError(f"Unexpected node type in FormulaConstruct: {type(node)!r}")

    # self._parts is always a list[Node]
    for group in _groups(self._parts):
        yield tuple(group)
__iter__()

Alias for iter_summands().

Source code in brmspy/types/formula_dsl.py
def __iter__(self) -> Iterator[Summand]:
    """Alias for [`iter_summands()`][brmspy.types.formula_dsl.FormulaConstruct.iter_summands]."""
    return self.iter_summands()
iterate()

Iterate over all leaf nodes in left-to-right order.

This flattens the expression tree, unlike iter_summands(), which respects grouping.

Returns:

Type Description
Iterator[FormulaPart | ProxyListSexpVector]
Source code in brmspy/types/formula_dsl.py
def iterate(self) -> Iterator[FormulaPart | ProxyListSexpVector]:
    """
    Iterate over all leaf nodes in left-to-right order.

    This flattens the expression tree, unlike
    [`iter_summands()`][brmspy.types.formula_dsl.FormulaConstruct.iter_summands], which
    respects grouping.

    Returns
    -------
    Iterator[FormulaPart | ProxyListSexpVector]
    """

    def _walk(node: Node) -> Iterator[FormulaPart | ProxyListSexpVector]:
        if isinstance(node, FormulaPart):
            yield node
        elif isinstance(node, ProxyListSexpVector):
            yield node
        elif isinstance(node, list):
            for child in node:
                yield from _walk(child)
        else:
            raise TypeError(
                f"Unexpected node type in FormulaConstruct: {type(node)!r}"
            )

    for root in self._parts:
        yield from _walk(root)
__str__()
Source code in brmspy/types/formula_dsl.py
def __str__(self) -> str:
    return self._pretty(self._parts)
_pretty(node, _outer=True)
Source code in brmspy/types/formula_dsl.py
def _pretty(self, node, _outer=True) -> str:
    if isinstance(node, FormulaPart):
        return str(node)

    if isinstance(node, (ProxyListSexpVector, Sexp)):
        return _sexp_to_str(node)

    if isinstance(node, list):
        # Pretty-print each child
        rendered = [self._pretty(child, _outer=False) for child in node]

        # If only one child, no parentheses needed
        if len(rendered) == 1:
            return rendered[0]

        # Multiple children → join with " + "
        inner = " + ".join(rendered)
        if _outer:
            return inner
        else:
            return f"({inner})"

    raise TypeError(f"Unexpected node type {type(node)!r} in pretty-printer")
__repr__()
Source code in brmspy/types/formula_dsl.py
def __repr__(self) -> str:
    return self.__str__()
__init__(_parts)

Functions

log(*msg, method_name=None, level=logging.INFO)

Log a message with automatic method name detection.

Parameters:

Name Type Description Default
msg str

The message to log

()
method_name str

The name of the method/function. If None, will auto-detect from call stack.

None
level int

Logging level (default: logging.INFO)

INFO
Source code in brmspy/helpers/log.py
def log(*msg: str, method_name: str | None = None, level: int = logging.INFO):
    """
    Log a message with automatic method name detection.

    Parameters
    ----------
    msg : str
        The message to log
    method_name : str, optional
        The name of the method/function. If None, will auto-detect from call stack.
    level : int, optional
        Logging level (default: logging.INFO)
    """
    if method_name is None:
        method_name = _get_caller_name()

    msg_str = " ".join(str(v) for v in msg)

    logger = get_logger()
    logger.log(level, msg_str, extra={"method_name": method_name})

log_warning(msg, method_name=None)

Log a warning message.

Parameters:

Name Type Description Default
msg str

The warning message to log

required
method_name str

The name of the method/function. If None, will auto-detect from call stack.

None
Source code in brmspy/helpers/log.py
def log_warning(msg: str, method_name: str | None = None):
    """
    Log a warning message.

    Parameters
    ----------
    msg : str
        The warning message to log
    method_name : str, optional
        The name of the method/function. If None, will auto-detect from call stack.

    """
    log(msg, method_name=method_name, level=logging.WARNING)

brmsfit_to_idata(brmsfit_obj, model_data=None)

Source code in brmspy/helpers/_rpy2/_conversion.py
def brmsfit_to_idata(brmsfit_obj, model_data=None) -> IDBrm:
    posterior_dict, _ = _brmsfit_get_posterior(brmsfit_obj)
    resp_names = _brmsfit_get_response_names(brmsfit_obj)
    dims, coords = _brmsfit_get_dims_and_coords(brmsfit_obj, resp_names=resp_names)
    observed_data_dict = _brmsfit_get_observed_data(brmsfit_obj, resp_names)
    post_pred_dict, _ = _brmsfit_get_predict_generic(
        brmsfit_obj, function="brms::posterior_predict", resp_names=resp_names
    )
    log_lik_dict, _ = _brmsfit_get_predict_generic(
        brmsfit_obj, function="brms::log_lik", resp_names=resp_names
    )
    constant_data_dict = _brmsfit_get_constant_data(
        brmsfit_obj, newdata=None, resp_names=resp_names
    )
    for name in constant_data_dict:
        if name not in dims:
            dims[name] = ["obs_id"]

    idata = az.from_dict(
        posterior=posterior_dict,
        posterior_predictive=post_pred_dict or None,
        log_likelihood=log_lik_dict or None,
        observed_data=observed_data_dict or None,
        coords=coords or None,
        constant_data=constant_data_dict or None,
        dims=dims or None,
    )

    return cast(IDBrm, idata)

kwargs_r(kwargs)

Convert Python keyword arguments to R-compatible format.

Convenience function that applies py_to_r() to all values in a keyword arguments dictionary, preparing them for R function calls.

Parameters:

Name Type Description Default
kwargs dict or None

Dictionary of keyword arguments where values may be Python objects (dicts, lists, DataFrames, arrays, etc.)

required

Returns:

Type Description
dict

Dictionary with same keys but R-compatible values, or empty dict if None

Notes

This is a thin wrapper around py_to_r() that operates on dictionaries. It's commonly used to prepare keyword arguments for R function calls via rpy2.

Examples:

from brmspy.helpers.conversion import kwargs_r
import pandas as pd
import numpy as np

# Prepare kwargs for R function
py_kwargs = {
    'data': pd.DataFrame({'y': [1, 2], 'x': [1, 2]}),
    'prior': {'b': [0, 1]},
    'chains': 4,
    'iter': 2000
}

r_kwargs = kwargs_r(py_kwargs)
# All values converted to R objects
# Can now call: r_function(**r_kwargs)
See Also

py_to_r : Underlying conversion function for individual values brmspy.brms.fit : Uses this to prepare user kwargs for R

Source code in brmspy/helpers/_rpy2/_conversion.py
def kwargs_r(kwargs: dict | None) -> dict:
    """
    Convert Python keyword arguments to R-compatible format.

    Convenience function that applies py_to_r() to all values in a
    keyword arguments dictionary, preparing them for R function calls.

    Parameters
    ----------
    kwargs : dict or None
        Dictionary of keyword arguments where values may be Python objects
        (dicts, lists, DataFrames, arrays, etc.)

    Returns
    -------
    dict
        Dictionary with same keys but R-compatible values, or empty dict if None

    Notes
    -----
    This is a thin wrapper around `py_to_r()` that operates on dictionaries.
    It's commonly used to prepare keyword arguments for R function calls via rpy2.

    Examples
    --------

    ```python
    from brmspy.helpers.conversion import kwargs_r
    import pandas as pd
    import numpy as np

    # Prepare kwargs for R function
    py_kwargs = {
        'data': pd.DataFrame({'y': [1, 2], 'x': [1, 2]}),
        'prior': {'b': [0, 1]},
        'chains': 4,
        'iter': 2000
    }

    r_kwargs = kwargs_r(py_kwargs)
    # All values converted to R objects
    # Can now call: r_function(**r_kwargs)
    ```

    See Also
    --------
    py_to_r : Underlying conversion function for individual values
    brmspy.brms.fit : Uses this to prepare user kwargs for R
    """
    if kwargs is None:
        return {}
    return {k: py_to_r(v) for k, v in kwargs.items()}

py_to_r(obj)

Convert arbitrary Python objects to R objects via rpy2.

Comprehensive converter that handles nested structures (dicts, lists), DataFrames, arrays, and scalars. Uses rpy2's converters with special handling for dictionaries (→ R named lists) and lists of dicts.

Parameters:

Name Type Description Default
obj any

Python object to convert. Supported types: - None → R NULL - dict → R named list (ListVector), recursively - list/tuple of dicts → R list of named lists - list/tuple (other) → R vector or list - pd.DataFrame → R data.frame - np.ndarray → R vector/matrix - scalars (int, float, str, bool) → R atomic types

required

Returns:

Type Description
rpy2 R object

R representation of the Python object

Notes

Conversion Rules:

  1. None: → R NULL
  2. DataFrames: → R data.frame (via pandas2ri)
  3. Dictionaries: → R named list (ListVector), recursively converting values
  4. Lists of dicts: → R list with 1-based indexed names containing named lists
  5. Other lists/tuples: → R vectors or lists (via rpy2 default)
  6. NumPy arrays: → R vectors/matrices (via numpy2ri)
  7. Scalars: → R atomic values

Recursive Conversion:

Dictionary values are recursively converted, allowing nested structures:

{'a': {'b': [1, 2, 3]}}    list(a = list(b = c(1, 2, 3)))

List of Dicts:

Lists containing only dicts are converted to R lists with 1-based indexing:

[{'x': 1}, {'x': 2}]    list("1" = list(x = 1), "2" = list(x = 2))

Examples:

from brmspy.helpers.conversion import py_to_r
import numpy as np
import pandas as pd

# Scalars
py_to_r(5)        # R: 5
py_to_r("hello")  # R: "hello"
py_to_r(None)     # R: NULL

# Arrays
py_to_r(np.array([1, 2, 3]))  # R: c(1, 2, 3)

# DataFrames
df = pd.DataFrame({'x': [1, 2], 'y': [3, 4]})
py_to_r(df)  # R: data.frame(x = c(1, 2), y = c(3, 4))
See Also

r_to_py : Convert R objects back to Python kwargs_r : Convert keyword arguments dict for R function calls brmspy.brms.fit : Uses this for converting data to R

Source code in brmspy/helpers/_rpy2/_converters/_dispatch.py
def py_to_r(obj: PyObject) -> Sexp:
    """
    Convert arbitrary Python objects to R objects via rpy2.

    Comprehensive converter that handles nested structures (dicts, lists),
    DataFrames, arrays, and scalars. Uses rpy2's converters with special
    handling for dictionaries (→ R named lists) and lists of dicts.

    Parameters
    ----------
    obj : any
        Python object to convert. Supported types:
        - None → R NULL
        - dict → R named list (ListVector), recursively
        - list/tuple of dicts → R list of named lists
        - list/tuple (other) → R vector or list
        - pd.DataFrame → R data.frame
        - np.ndarray → R vector/matrix
        - scalars (int, float, str, bool) → R atomic types

    Returns
    -------
    rpy2 R object
        R representation of the Python object

    Notes
    -----
    **Conversion Rules:**

    1. **None**: → R NULL
    2. **DataFrames**: → R data.frame (via pandas2ri)
    3. **Dictionaries**: → R named list (ListVector), recursively converting values
    4. **Lists of dicts**: → R list with 1-based indexed names containing named lists
    5. **Other lists/tuples**: → R vectors or lists (via rpy2 default)
    6. **NumPy arrays**: → R vectors/matrices (via numpy2ri)
    7. **Scalars**: → R atomic values

    **Recursive Conversion:**

    Dictionary values are recursively converted, allowing nested structures:
    ```python
    {'a': {'b': [1, 2, 3]}}  →  list(a = list(b = c(1, 2, 3)))
    ```

    **List of Dicts:**

    Lists containing only dicts are converted to R lists with 1-based indexing:
    ```python
    [{'x': 1}, {'x': 2}]  →  list("1" = list(x = 1), "2" = list(x = 2))
    ```

    Examples
    --------

    ```python
    from brmspy.helpers.conversion import py_to_r
    import numpy as np
    import pandas as pd

    # Scalars
    py_to_r(5)        # R: 5
    py_to_r("hello")  # R: "hello"
    py_to_r(None)     # R: NULL

    # Arrays
    py_to_r(np.array([1, 2, 3]))  # R: c(1, 2, 3)

    # DataFrames
    df = pd.DataFrame({'x': [1, 2], 'y': [3, 4]})
    py_to_r(df)  # R: data.frame(x = c(1, 2), y = c(3, 4))
    ```

    See Also
    --------
    r_to_py : Convert R objects back to Python
    kwargs_r : Convert keyword arguments dict for R function calls
    brmspy.brms.fit : Uses this for converting data to R
    """
    import rpy2.robjects as ro

    if obj is None:
        return ro.NULL

    if isinstance(obj, ro.Sexp):
        return obj

    if isinstance(obj, RListVectorExtension) and isinstance(obj.r, ro.Sexp):
        return obj.r

    _type = type(obj)
    converter = None

    if _type in _registry._PY2R_CONVERTERS:
        # O(1) lookup first
        converter = _registry._PY2R_CONVERTERS[_type]
    else:
        for _type, _con in _registry._PY2R_CONVERTERS.items():
            if isinstance(obj, _type):
                converter = _con
                break

    assert len(_registry._PY2R_CONVERTERS) > 0, "NO PY2R CONVERTERS"
    assert (
        converter
    ), "object fallback must be in place in __init__.py! This is an issue with the library, not the user!"
    return converter(obj)

_build_priors(priors=None)

Build R brms prior object from Python PriorSpec specifications.

Converts a sequence of PriorSpec objects to a single combined R brms prior object by calling brms::prior_string() for each spec and combining with +. Used internally by fit() to translate Python prior specifications to R.

Parameters:

Name Type Description Default
priors sequence of PriorSpec

List of prior specifications. Each PriorSpec contains: - prior: Prior distribution string (e.g., "normal(0, 1)") - class_: Parameter class (e.g., "b", "Intercept", "sigma") - coef: Specific coefficient name (optional) - group: Group-level effects (optional)

If None or empty, returns empty list (brms uses default priors)

None

Returns:

Type Description
R brmsprior object or list

Combined R brms prior object if priors provided, empty list otherwise

Raises:

Type Description
AssertionError

If combined result is not a valid brmsprior object

Notes

Prior Combination:

Multiple priors are combined using R's + operator:

prior1 + prior2 + prior3

This creates a single brmsprior object containing all specifications.

brms Prior Classes:

Common parameter classes: - b: Population-level effects (regression coefficients) - Intercept: Model intercept - sigma: Residual standard deviation (for gaussian family) - sd: Standard deviation of group-level effects - cor: Correlation of group-level effects

Prior String Format:

brms uses Stan-style prior specifications: - Normal: "normal(mean, sd)" - Student-t: "student_t(df, location, scale)" - Cauchy: "cauchy(location, scale)" - Exponential: "exponential(rate)" - Uniform: "uniform(lower, upper)"

Examples:

from brmspy.types import PriorSpec
from brmspy.helpers.priors import _build_priors

# Single prior for regression coefficients
priors = [
    PriorSpec(
        prior="normal(0, 1)",
        class_="b"
    )
]
brms_prior = _build_priors(priors)
See Also

brmspy.types.PriorSpec : Prior specification class brmspy.brms.fit : Uses this to convert priors for model fitting brms::prior : R brms prior specification brms::set_prior : R function for setting priors

References

.. [1] brms prior documentation: https://paul-buerkner.github.io/brms/reference/set_prior.html .. [2] Stan prior choice recommendations: https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations

Source code in brmspy/helpers/_rpy2/_priors.py
def _build_priors(
    priors: None | Sequence[PriorSpec] = None,
) -> list[Sexp]:
    """
    Build R brms prior object from Python PriorSpec specifications.

    Converts a sequence of PriorSpec objects to a single combined R brms prior
    object by calling brms::prior_string() for each spec and combining with `+`.
    Used internally by fit() to translate Python prior specifications to R.

    Parameters
    ----------
    priors : sequence of PriorSpec, optional
        List of prior specifications. Each PriorSpec contains:
        - prior: Prior distribution string (e.g., "normal(0, 1)")
        - class_: Parameter class (e.g., "b", "Intercept", "sigma")
        - coef: Specific coefficient name (optional)
        - group: Group-level effects (optional)

        If None or empty, returns empty list (brms uses default priors)

    Returns
    -------
    R brmsprior object or list
        Combined R brms prior object if priors provided, empty list otherwise

    Raises
    ------
    AssertionError
        If combined result is not a valid brmsprior object

    Notes
    -----
    **Prior Combination:**

    Multiple priors are combined using R's `+` operator:
    ```R
    prior1 + prior2 + prior3
    ```

    This creates a single brmsprior object containing all specifications.

    **brms Prior Classes:**

    Common parameter classes:
    - **b**: Population-level effects (regression coefficients)
    - **Intercept**: Model intercept
    - **sigma**: Residual standard deviation (for gaussian family)
    - **sd**: Standard deviation of group-level effects
    - **cor**: Correlation of group-level effects

    **Prior String Format:**

    brms uses Stan-style prior specifications:
    - Normal: "normal(mean, sd)"
    - Student-t: "student_t(df, location, scale)"
    - Cauchy: "cauchy(location, scale)"
    - Exponential: "exponential(rate)"
    - Uniform: "uniform(lower, upper)"

    Examples
    --------

    ```python
    from brmspy.types import PriorSpec
    from brmspy.helpers.priors import _build_priors

    # Single prior for regression coefficients
    priors = [
        PriorSpec(
            prior="normal(0, 1)",
            class_="b"
        )
    ]
    brms_prior = _build_priors(priors)
    ```

    See Also
    --------
    brmspy.types.PriorSpec : Prior specification class
    brmspy.brms.fit : Uses this to convert priors for model fitting
    brms::prior : R brms prior specification
    brms::set_prior : R function for setting priors

    References
    ----------
    .. [1] brms prior documentation: https://paul-buerkner.github.io/brms/reference/set_prior.html
    .. [2] Stan prior choice recommendations: https://github.com/stan-dev/stan/wiki/Prior-Choice-Recommendations
    """
    if not priors:
        return []
    import rpy2.robjects as ro

    from brmspy.helpers._rpy2._converters import r_to_py

    fun_prior_string = cast(Callable, ro.r("brms::prior_string"))

    prior_objs = []
    for p in priors:
        kwargs = p.to_brms_kwargs()
        # first argument is the prior string
        prior_str = kwargs.pop("prior")
        prior_obj = fun_prior_string(prior_str, **kwargs)
        prior_objs.append(prior_obj)

    brms_prior = prior_objs[0]
    for p in prior_objs[1:]:
        brms_prior = brms_prior + p

    return brms_prior

_execute_formula(formula)

Source code in brmspy/_brms_functions/formula.py
def _execute_formula(formula: FormulaConstruct | Sexp | str) -> Sexp:
    import rpy2.robjects as ro

    if isinstance(formula, Sexp):
        return formula
    if isinstance(formula, str):
        formula = FormulaConstruct._formula_parse(formula)

    # Must run for formula functions, e.g me() to register
    ro.r("library(brms)")

    fun_add = cast(Callable[[Sexp, Sexp], Sexp], ro.r("function (a, b) a + b"))

    result: Sexp | None = None
    for summand in formula:
        subresult: Sexp = py_to_r(summand[0])
        for part in summand[1:]:
            subresult = fun_add(subresult, py_to_r(part))

        if result is None:
            result = subresult
        else:
            result = fun_add(result, subresult)

    assert result is not None
    return result

bf(*formulas, **formula_args)

Build a brms model formula.

This is the primary entrypoint for specifying the mean model and can be combined with other formula parts (e.g. lf, nlf, acformula) using +.

Parameters:

Name Type Description Default
*formulas str

One or more brms formula strings (e.g. "y ~ x + (1|group)"). Multiple formulas are commonly used for multivariate models.

()
**formula_args

Keyword arguments forwarded to R brms::brmsformula() (for example decomp="QR", center=True, sparse=True, nl=True, loop=True).

{}

Returns:

Type Description
FormulaConstruct

A composable formula specification.

See Also

brms::brmsformula : R documentation

Examples:

Basic formula:

from brmspy.brms import bf

f = bf("y ~ x1 + x2 + (1|group)")

QR decomposition (often helps with collinearity):

from brmspy.brms import bf

f = bf("reaction ~ days + (days|subject)", decomp="QR")

Multivariate formula + residual correlation:

from brmspy.brms import bf, set_rescor

f = bf("mvbind(y1, y2) ~ x") + set_rescor(True)
Source code in brmspy/_brms_functions/formula.py
def bf(*formulas: str, **formula_args) -> FormulaConstruct:
    """
    Build a brms model formula.

    This is the primary entrypoint for specifying the mean model and can be
    combined with other formula parts (e.g. `lf`, `nlf`, `acformula`) using ``+``.

    Parameters
    ----------
    *formulas : str
        One or more brms formula strings (e.g. ``"y ~ x + (1|group)"``). Multiple
        formulas are commonly used for multivariate models.
    **formula_args
        Keyword arguments forwarded to R ``brms::brmsformula()`` (for example
        ``decomp="QR"``, ``center=True``, ``sparse=True``, ``nl=True``, ``loop=True``).

    Returns
    -------
    FormulaConstruct
        A composable formula specification.

    See Also
    --------
    brms::brmsformula : [R documentation](https://paulbuerkner.com/brms/reference/brmsformula.html)

    Examples
    --------
    Basic formula:

    ```python
    from brmspy.brms import bf

    f = bf("y ~ x1 + x2 + (1|group)")
    ```

    QR decomposition (often helps with collinearity):

    ```python
    from brmspy.brms import bf

    f = bf("reaction ~ days + (days|subject)", decomp="QR")
    ```

    Multivariate formula + residual correlation:

    ```python
    from brmspy.brms import bf, set_rescor

    f = bf("mvbind(y1, y2) ~ x") + set_rescor(True)
    ```
    """
    part = FormulaPart(_fun="bf", _args=list(formulas), _kwargs=formula_args)
    return FormulaConstruct._formula_parse(part)

brm(formula, data, priors=None, family='gaussian', sample_prior='no', sample=True, backend='cmdstanr', formula_args=None, cores=2, *, return_idata=True, **brm_args)

brm(formula: FormulaConstruct | ProxyListSexpVector | str, data: dict | pd.DataFrame, priors: Sequence[PriorSpec] | None = ..., family: str | ListSexpVector | None = ..., sample_prior: str = ..., sample: bool = ..., backend: str = ..., formula_args: dict | None = ..., cores: int | None = ..., *, return_idata: Literal[True] = True, **brm_args: Any) -> FitResult
brm(formula: FormulaConstruct | ProxyListSexpVector | str, data: dict | pd.DataFrame, priors: Sequence[PriorSpec] | None = ..., family: str | ListSexpVector | None = ..., sample_prior: str = ..., sample: bool = ..., backend: str = ..., formula_args: dict | None = ..., cores: int | None = ..., *, return_idata: Literal[False], **brm_args: Any) -> ProxyListSexpVector

Fit a Bayesian regression model with brms.

This is a thin wrapper around R brms::brm() that returns a structured FitResult (including an ArviZ InferenceData).

Parameters:

Name Type Description Default
formula str or FormulaConstruct

Model formula. Accepts a plain brms formula string (e.g. "y ~ x + (1|g)") or a composed formula created via brmspy.brms.bf() / brmspy.brms.lf() (typically imported as from brmspy.brms import bf, lf).

required
data dict or DataFrame

Model data.

required
priors Sequence[PriorSpec] or None

Optional prior specifications created via brmspy.brms.prior().

None
family str or ListSexpVector or None

brms family specification (e.g. "gaussian", "poisson").

"gaussian"
sample_prior str

Passed to brms. Common values: "no", "yes", "only".

"no"
sample bool

If False, compile the model without sampling (brms empty=TRUE).

True
backend str

Stan backend. Common values: "cmdstanr" or "rstan".

"cmdstanr"
formula_args dict or None

Reserved for future use. Currently ignored.

None
cores int or None

Number of cores for brms/cmdstanr.

2
return_idata bool

When working with large datasets, you might not want the full idata. when False, you get the R object proxy which can be forwarded to posterior_epred or other functions

True
**brm_args

Additional keyword arguments passed to R brms::brm() (e.g. chains, iter, warmup, seed).

{}

Returns:

Type Description
FitResult

Result object with idata (ArviZ InferenceData) and an underlying R handle.

See Also

brms::brm : R documentation

Warnings

Using cores <= 1 can be unstable in embedded R sessions and may crash the worker process. Prefer cores >= 2.

Examples:

from brmspy import brms

fit = brms.brm("y ~ x + (1|g)", data=df, family="gaussian", chains=4, cores=4)

fit.idata.posterior
Source code in brmspy/_brms_functions/brm.py
def brm(
    formula: FormulaConstruct | ProxyListSexpVector | str,
    data: dict | pd.DataFrame,
    priors: Sequence[PriorSpec] | None = None,
    family: str | ListSexpVector | None = "gaussian",
    sample_prior: str = "no",
    sample: bool = True,
    backend: str = "cmdstanr",
    formula_args: dict | None = None,
    cores: int | None = 2,
    *,
    return_idata: bool = True,
    **brm_args,
) -> FitResult | ProxyListSexpVector:
    """
    Fit a Bayesian regression model with brms.

    This is a thin wrapper around R ``brms::brm()`` that returns a structured
    `FitResult` (including an ArviZ `InferenceData`).

    Parameters
    ----------
    formula : str or FormulaConstruct
        Model formula. Accepts a plain brms formula string (e.g. ``"y ~ x + (1|g)"``)
        or a composed formula created via `brmspy.brms.bf()` / `brmspy.brms.lf()`
        (typically imported as ``from brmspy.brms import bf, lf``).
    data : dict or pandas.DataFrame
        Model data.
    priors : Sequence[PriorSpec] or None, default=None
        Optional prior specifications created via `brmspy.brms.prior()`.
    family : str or rpy2.rinterface.ListSexpVector or None, default="gaussian"
        brms family specification (e.g. ``"gaussian"``, ``"poisson"``).
    sample_prior : str, default="no"
        Passed to brms. Common values: ``"no"``, ``"yes"``, ``"only"``.
    sample : bool, default=True
        If ``False``, compile the model without sampling (brms ``empty=TRUE``).
    backend : str, default="cmdstanr"
        Stan backend. Common values: ``"cmdstanr"`` or ``"rstan"``.
    formula_args : dict or None, default=None
        Reserved for future use. Currently ignored.
    cores : int or None, default=2
        Number of cores for brms/cmdstanr.
    return_idata : bool, default True
        When working with large datasets, you might not want the full idata.
        when False, you get the R object proxy which can be forwarded to posterior_epred
        or other functions
    **brm_args
        Additional keyword arguments passed to R ``brms::brm()`` (e.g. ``chains``,
        ``iter``, ``warmup``, ``seed``).

    Returns
    -------
    FitResult
        Result object with `idata` (ArviZ `InferenceData`) and an underlying R handle.

    See Also
    --------
    brms::brm : [R documentation](https://paulbuerkner.com/brms/reference/brm.html)

    Warnings
    --------
    Using ``cores <= 1`` can be unstable in embedded R sessions and may crash the
    worker process. Prefer ``cores >= 2``.

    Examples
    --------
    ```python
    from brmspy import brms

    fit = brms.brm("y ~ x + (1|g)", data=df, family="gaussian", chains=4, cores=4)

    fit.idata.posterior
    ```
    """
    import rpy2.robjects as ro
    import rpy2.robjects.packages as packages

    fun_brm = cast(Callable, ro.r("brms::brm"))

    if backend == "cmdstanr":
        try:
            cmdstanr = packages.importr("cmdstanr")
        except:
            cmdstanr = None
        if cmdstanr is None:
            raise RuntimeError(
                "cmdstanr backend is not installed! Please run install_brms(install_cmdstanr=True)"
            )

    if backend == "rstan":
        try:
            rstan = packages.importr("rstan")
        except:
            rstan = None
        if rstan is None:
            raise RuntimeError(
                "rstan backend is not installed! Please run install_brms(install_rstan=True)"
            )

    # Formula checks. These should never be reached in the first place
    # if they are, the library is calling brm() from main directly without remote call
    assert not isinstance(formula, SexpWrapper)
    assert formula is not None
    if formula_args and isinstance(formula, str):
        formula = bf(formula, **formula_args)

    formula_obj = _execute_formula(formula)

    # Convert data to R format
    data_r = py_to_r(data)

    # Setup priors
    brms_prior = _build_priors(priors)

    # Prepare brm() arguments
    brm_kwargs: dict[str, Any] = {
        "formula": formula_obj,
        "data": data_r,
        "family": family,
        "sample_prior": sample_prior,
        "backend": backend,
        "cores": cores,
    }

    # Add priors if specified
    if len(brms_prior) > 0:
        brm_kwargs["prior"] = brms_prior

    # Add user-specified arguments
    brm_kwargs.update(brm_args)

    brm_kwargs = kwargs_r(brm_kwargs)

    # Set empty=TRUE if not sampling
    if not sample:
        brm_kwargs["empty"] = True
        log("Creating empty r object (no sampling)...")
    else:
        log(f"Fitting model with brms (backend: {backend})...")

    # Call brms::brm() with all arguments
    fit = fun_brm(**brm_kwargs)

    log("Fit done!")

    # Handle return type conversion
    if not return_idata:
        return fit

    if not sample:
        return FitResult(idata=IDBrm(), r=fit)

    idata = brmsfit_to_idata(fit)
    return FitResult(idata=idata, r=fit)