Source code for biogeme.tools.pymc_utils

[docs] def pretty_model(model): """Print a human-readable snapshot of a PyMC model (version-robust). This avoids `model.describe()` (not present in all versions) and instead inspects the standard attributes exposed by `pm.Model`. """ # Helper: summarize an RV def _rv_row(rv, kind): op = getattr(getattr(rv, "owner", None), "op", None) dist = getattr(op, "name", None) or type(op).__name__ # Robust shape representation: avoid truth-testing tensors and int-casting symbolic dims shp_attr = getattr(rv, "shape", ()) try: shp_iter = tuple(shp_attr) # may contain symbolic dims except (TypeError, ValueError, AttributeError): shp_iter = () shp = tuple(s if isinstance(s, int) else str(s) for s in shp_iter) return { "name": rv.name, "kind": kind, "dist": dist, "shape": shp, "dtype": str(getattr(rv, "dtype", "?")), } rows = [] for rv in getattr(model, "free_RVs", []): rows.append(_rv_row(rv, "free")) for rv in getattr(model, "observed_RVs", []): rows.append(_rv_row(rv, "observed")) for d in getattr(model, "deterministics", []): rows.append( { "name": d.name, "kind": "deterministic", "dist": "Deterministic", # Avoid boolean evaluation on tensor shapes; stringify symbolic dims "shape": ( lambda _a: ( ( lambda _it: tuple( _s if isinstance(_s, int) else str(_s) for _s in _it ) )(tuple(_a) if not isinstance(_a, (int,)) else (_a,)) ) )(getattr(d, "shape", ())), "dtype": str(getattr(d, "dtype", "?")), } ) # Pretty print table without pandas dependency print("\n=== Model variables ===") if not rows: print("(no variables found)") else: headers = ("name", "kind", "dist", "shape", "dtype") # compute column widths def _fmt(val): if isinstance(val, tuple): return str(val) return str(val) widths = {h: max(len(h), max(len(_fmt(r[h])) for r in rows)) for h in headers} # header line = " ".join(h.ljust(widths[h]) for h in headers) print(line) print(" ".join("-" * widths[h] for h in headers)) # rows for r in rows: print(" ".join(_fmt(r[h]).ljust(widths[h]) for h in headers)) # Lists by category print("\nFREE RVs :", [rv.name for rv in getattr(model, "free_RVs", [])]) print("DETERMIN.:", [d.name for d in getattr(model, "deterministics", [])]) print("OBSERVED :", [rv.name for rv in getattr(model, "observed_RVs", [])]) # Try evaluating logp at the initial point (value space, not transformed) try: # Prefer explicit value variables to avoid transform name mismatches (e.g., *_interval__) rvs_to_values = getattr(model, "rvs_to_values", {}) if rvs_to_values: val_vars = list(rvs_to_values.values()) else: val_vars = list(getattr(model, "value_vars", [])) if not val_vars: raise AttributeError("No value variables available to compile logp.") # Compile logp expecting the untransformed value variables as inputs logp_fn = model.compile_logp(inputs=val_vars) # Build an input point in value space using each variable's test_value ip_val = {} for v in val_vars: tv = getattr(getattr(v, "tag", object()), "test_value", None) if tv is None: raise ValueError( f"No test_value found for value var '{getattr(v, 'name', '?')}'." ) ip_val[v.name] = tv lp = float(logp_fn(ip_val)) print("\nlogp(initial point):", lp) except (AttributeError, KeyError, TypeError, ValueError) as e: print("\n(logp at initial point not available)", e)