Source code for sim_panel.outcomes.deterministic
from __future__ import annotations
import hashlib
from typing import Any, Dict, Optional
from sim_panel.outcomes.base import EvaluationContext, OutcomeConfig, OutcomeResult
from sim_panel.outcomes.specs import QuestionnaireSpec, FieldSpec
[docs]
class DeterministicOutcomeModel:
"""
Deterministic questionnaire filler for CI/tests.
Strategy:
- For categorical / choices fields: pick the first choice deterministically (or hash-index if desired).
- For int/float: hash into range if provided, else 0/0.0.
- For bool: hash parity.
- For text: short templated stub.
"""
def __init__(self, cfg: OutcomeConfig) -> None:
self.cfg = cfg
[docs]
def evaluate(self, *, panelist, ctx: EvaluationContext, prompting_strategy: str = "persona") -> OutcomeResult:
q = self.cfg.questionnaire
seed = f"{ctx.panelist_id}|{ctx.product_id}|{ctx.t}".encode("utf-8")
h = hashlib.blake2b(seed, digest_size=8).hexdigest()
u = int(h, 16)
outcomes: Dict[str, Any] = {}
for fs in q.outcome_fields:
outcomes[fs.name] = _fill_field(fs, u)
traces: Dict[str, Any] = {}
for fs in q.trace_fields:
traces[fs.name] = _fill_trace_field(fs, u, ctx)
return OutcomeResult(
outcomes=outcomes,
traces=traces if q.trace_fields else {},
raw_text=None,
errors=[],
)
def _fill_field(fs: FieldSpec, u: int) -> Any:
if fs.choices:
# Stable but not always first: index by hash
idx = u % len(fs.choices)
return fs.choices[idx]
if fs.type == "int":
lo = int(fs.min_value) if fs.min_value is not None else 0
hi = int(fs.max_value) if fs.max_value is not None else lo + 10
if hi < lo:
hi = lo
span = max(1, hi - lo + 1)
return lo + (u % span)
if fs.type == "float":
lo = float(fs.min_value) if fs.min_value is not None else 0.0
hi = float(fs.max_value) if fs.max_value is not None else lo + 1.0
if hi < lo:
hi = lo
# map u to [0,1)
frac = (u % 10_000) / 10_000.0
return lo + (hi - lo) * frac
if fs.type == "bool":
return bool(u % 2)
if fs.type == "categorical":
# categorical without choices is ill-formed; return a placeholder
return "option"
if fs.type == "text":
return "..."
if fs.type == "json":
return {}
return None
def _fill_trace_field(fs: FieldSpec, u: int, ctx: EvaluationContext) -> Any:
if fs.type == "text":
# Short stable stub
return f"[deterministic trace] panelist={ctx.panelist_id} product={ctx.product_id} t={ctx.t}"
if fs.type == "json":
return {"panelist_id": ctx.panelist_id, "product_id": ctx.product_id, "t": ctx.t, "seed": int(u % 1_000_000)}
# For other types, reuse general filler
return _fill_field(fs, u)