← Field notes
Note 01·Methodology · Engine·2026-06·v0.4 · draft·12 min

Not another panel. A market simulator.

The hybrid Bass-SIR-D engine: math, vectorisation, calibration, reproducibility — and why it runs in milliseconds with zero LLM calls per tick.

by Holon Research

ABSTRACT

We describe Holon's simulation engine, a continuous-time hazard model with four states (Susceptible, Adopter, Recovered, Detractor) operating over a directed weighted graph of 600–10,000 agents. Adoption combines an exogenous innovation term with endogenous social pressure (Bass [1]); churn is socially contagious (Nitzan & Libai [3]); negative signals carry a 2.5× weight (Baumeister [6]). Because every quantity reduces to a sparse matrix-vector product, a 90-tick run on 1,000 agents executes in ~9 ms on a single core (measured — §7). Runs are deterministic functions of (scenario, seed) and hashable end-to-end — the precondition for publishing backtests other people can verify.

1. The problem with synthetic panels

Most synthetic-user tools prompt a language model to roleplay a persona, then ask it questions one at a time. It is a chat window wearing a costume — fine for a first gut-check, useless for the thing that actually decides a launch: how an opinion moves through a room. Adoption is not an average of isolated answers. It is a process on a network.

The 2026 buyer guides for synthetic research are explicit on the gap: the category is weakest precisely where it is most often sold — modelling social contagion, negative word-of-mouth, and the cascades that determine whether a feature lands or a price change rebounds [9]. Holon was built against that gap.

Per 90-day run
~9 ms
1,000 agents · single core · measured
LLM calls
0
per tick · per run · per agent
Edges modeled
~50k
directed · weighted · sparse

2. The four-state model

Each of n agents occupies one of four states at discrete time t:

  • S — Susceptible. Has not adopted. Receives both positive and negative pressure.
  • I — Adopter (Infectious). Has adopted. Emits a persuasion signal η to its out-neighbours.
  • R — Recovered. Has churned silently. Emits nothing.
  • D — Detractor. Has churned loudly. Emits a negative signal weighted ν ≈ 2.5·η.

Transitions S→I, I→R, I→D, and D→R are stochastic and depend on the agent's role, budget, and the pressure arriving from its in-neighbours in a directed weighted graph G = (V, W). The split between R and D is governed by a role-specific loud-exit probability β (Power Users churn loudly; Lurkers vanish silent — §3 of Note 02 [11]).

Schema
Hazards drive every transition. β splits churn into loud (D) vs. silent (R); δ returns detractors to a quiet state on a ~14-day half-life [5].

3. Adoption hazard

We use a continuous-time hazard formulation discretised at Δt = 1 day. The probability that susceptible agent i adopts during a tick composes an exogenous innovation term p (advertising, discovery) with endogenous imitation q·φ⁺, divided by a role-based friction coefficient f, and dampened by negative pressure φ⁻:

S → I — show
λᵢ(t) = 1 − exp( −Δt · [ pᵢ + qᵢ · φ⁺ᵢ(t) / fᵢ⁽ᵉ⁾ − r · φ⁻ᵢ(t) ] )

The exponential form keeps λ in [0,1] for any pressure, avoids double-counting when multiple neighbours push simultaneously, and matches the standard continuous-time hazard convention used in epidemic and diffusion models [2]. The novelty is f (friction, see Note 02 [11]) and the explicit negative term r · φ⁻ that lets aversion suppress adoption even when social proof is high.

Prior values for (p, q) are taken from the Sultan-Farley-Lehmann meta-analysis [2], rescaled from annual category penetration to community-response hazards. Calibrated cities (Scale tier) fit per-agent (p, q, μ) to client historical curves and report a per-account MAPE on a held-out window.

4. Social pressure on the graph

Pressure is computed from the graph, not from a prompt. Let W ∈ ℝⁿˣⁿ₊ be the column-normalised weighted adjacency matrix where Wⱼᵢ > 0 means j influences i. Active adopters emit a signal u(t) = η ⊙ i(t); detractors emit a negative signal u⁻(t) = ν ⊙ d(t) with ν ≈ 2.5 η. The pressure received by every node is a single matrix product:

positive & negative pressure — show
φ⁺(t) = a ⊙ ( Wᵀ u(t) ) u(t) = η ⊙ i(t) φ⁻(t) = a ⊙ ( Wᵀ u⁻(t) ) u⁻(t) = ν ⊙ d(t)

Because each step is a sparse matrix-vector multiply, the whole city updates simultaneously. There is no per-agent loop, no per-agent API call. a is the receiver's affinity; ⊙ denotes element-wise product. Cluster-level ("tribal") pressure can be added with a membership matrix C ∈ {0,1}ⁿˣᴷ to model community contagion [4].

Schema
Pressure on every node = one sparse matrix-vector product. Filled cells = non-zero edge weights; the black column = adopter mask; the orange bars = the resulting pressure vector φ⁺.

5. Contagious churn

Naïve models treat churn as an independent coin flip. Holon makes it social: an adopter's churn hazard rises with personal aversion av and with the negative pressure from neighbours who already defected — but only if the agent is itself exposed (Nitzan & Libai's neighbour-defection effect [3]):

I → R or D — show
γᵢ(t) = 1 − exp( −Δt · [ μᵢ + μ_av·avᵢ + s · φ⁻ᵢ(t) · (0.15 + 1.3·avᵢ) ] )

The (0.15 + 1.3 av) factor captures Nitzan & Libai's finding that the social-churn effect is near-zero for satisfied customers. When churn fires, a role-specific β decides whether the agent exits quietly (R) or loudly (D). Detractors decay back to quiet at rate δ ≈ 0.07/tick — a ~14-day half-life calibrated on online-firestorm lifecycle studies [5].

6. Why it's fast

Every quantity above is a vector or a sparse matrix. A full 90-tick run on 1,000 agents is ~90 sparse matrix-vector products — milliseconds on a single core. Below is the inner loop in NumPy, the same code that powers the reference engine; the TypeScript port in /engine is a direct transliteration.

Show implementation — python
def step(state, W, p, q, r, mu, f, eta, nu, a, dt=1.0, rng=None):
    rng = rng or np.random.default_rng()
    I  = (state == 1).astype(float)
    D  = (state == 3).astype(float)
    u_pos =  eta * I
    u_neg =  nu  * D
    phi_pos = a * (W.T @ u_pos)          # positive social pressure
    phi_neg = a * (W.T @ u_neg)          # negative pressure (negativity bias)
    # adoption hazard (Bass-style with friction and aversion)
    lam = 1 - np.exp(-dt * np.maximum(0, p + q*phi_pos/f - r*phi_neg))
    # churn hazard, gated by personal aversion (Nitzan & Libai)
    gam = 1 - np.exp(-dt * (mu + 0.04*aversion + 0.14*phi_neg*(0.15+1.3*aversion)))
    return advance(state, lam, gam, beta_role, delta=0.07, rng=rng)

Zero LLM calls per tick means: no rate limits, no latency, no per-run bill. You can sweep 500 random seeds and read a distribution rather than a single hopeful point.

PER VERDICT · 1,000 AGENTS · 90 DAYS
Survey panelLLM panelSmallville-style [7]Holon
Wall-clock2–6 weeks~2 minhours~9 ms
Cost / run$5k–20k~$0.50$$$~$0
LLM calls0~n~n²0
Models networknonopartialyes
Reproduciblenononoseed→hash

* Holon wall-clock and cost measured on a single CPU core; LLM panel cost assumes GPT-4-class API at typical 2026 pricing.

Engine scaling · wall-clock per 90-tick run · measuredmax ≈ 106 ms
  • 1,000 agentsdemo city · |E|≈7k
    ~9 ms
  • 5,000 agentsmid · |E|≈34k
    ~53 ms
  • 10,000 agentslarge · |E|≈69k
    ~106 ms

7. Reproducibility

A run is a pure function of (scenario, seed). The engine uses a seeded Mulberry32 PRNG; Date.now() and Math.random() are forbidden inside it (linted at CI). We hash the final state, so any shared URL replays byte-for-byte — the precondition for publishing backtests that other people can independently verify.

Below is the measured battery — every row is the live output of the harness in scripts/engine-bench.ts, not a target we hope to hit.

PropertyMeasured result
Determinism200/200 — same (scenario, seed) → identical end-state hash; a different seed diverges
Run time · 1,000 agents · 90 ticks9 ms median · 10 ms p90 · single core
Scaling · 1k / 5k / 10k agents9 ms / 53 ms / 106 ms
Adoption spread · feature · 200 seedsp10 94% · p50 96% · p90 98%
Loud-detractor band · 200 seeds2.3% – 4.7% (p10–p90)
LLM calls · per tick · per run0

8. What the model misses

High-stakes decisions dominated by emotion or identity (politics, sensitive medical, security). Novel categories with no historical precedent. Rare-event tail risk. We log these limits per backtest in a mandatory "what the model misses" section. The model is a pre-filter, not an oracle — designed to kill bad ideas quickly so research budget can be spent on the survivors.

Built on research published in

arXiv
Science
INFORMS — Management Science
SAGE
Univ. of Chicago Press

Methodology builds on peer-reviewed research from the venues above. See references for exact papers.

References

  1. [1]
    Bass, F. (1969). A New Product Growth Model for Consumer Durables. Management Science 15(5).origin of p (innovation) and q (imitation)
  2. [2]
    Sultan, F., Farley, J. & Lehmann, D. (1990). A Meta-Analysis of Diffusion Models. Journal of Marketing Research.p̄≈0.03, q̄≈0.38 across categories — used as priors, rescaled to community-response hazards
  3. [3]
    Nitzan, I. & Libai, B. (2011). Social Effects on Customer Retention. Journal of Marketing 75(6).neighbour-defection raises churn ~1.3–1.8×, near-zero when satisfied
  4. [4]
    Granovetter, M. (1978). Threshold Models of Collective Behavior. AJS 83(6).thresholds for collective adoption
  5. [5]
    Pfeffer, J., Zorbach, T. & Carley, K. (2014). Understanding online firestorms. Journal of Marketing Communications.outrage decay ~7–21 days → δ≈0.07/tick
  6. [6]
    Baumeister, R. et al. (2001). Bad Is Stronger Than Good. Review of General Psychology 5(4).the 2.5× negativity-asymmetry constant
  7. [7]
  8. [8]
    Aral, S. & Walker, D. (2012). Identifying Influential and Susceptible Members of Social Networks. Science 337(6092).asymmetric influence/susceptibility — informs η variation by role
  9. [9]
    Synthetic Market Research Buyers Guide 2026 (Minds/Lakmoos/Aaru meta-review).category gap: social contagion + NWOM
  10. [10]
    Centola, D. & Macy, M. (2007). Complex Contagions and the Weakness of Long Ties. AJS 113(3).complex contagion → multi-source gate (see Note 02)
  11. [11]
    Holon Field Note 02 — Built to say no.the friction, β-split, and complex-contagion gate are detailed there

Reproducibility

Every figure in this note is reproducible from seed = 4711 on the engine in /engine. Run hashes ship with each release; deviations from the published hash are reportable bugs. The TypeScript and Python reference implementations are tested for hash parity at CI.

Want to run this on your market?

Request access
NEXT · Note 02
Built to say no.