{"id":50916217,"url":"https://github.com/borodark/smc_ex","last_synced_at":"2026-06-16T15:30:32.125Z","repository":{"id":349068859,"uuid":"1195603803","full_name":"borodark/smc_ex","owner":"borodark","description":"Sequential Monte Carlo methods for Elixir — Bootstrap Particle Filter, PMCMC, Online SMC². Streaming Bayesian inference on the BEAM.","archived":false,"fork":false,"pushed_at":"2026-04-04T02:44:59.000Z","size":258,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-04T04:25:17.789Z","etag":null,"topics":["bayesian-inference","beam","elixir","elixir-lang","elixir-library","epidemiology","particle-filter","pmcmc","probabilistic-programming","sequential-monte-carlo","state-estimation"],"latest_commit_sha":null,"homepage":"http://www.dataalienist.com/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/borodark.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-29T21:26:58.000Z","updated_at":"2026-04-04T02:45:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/borodark/smc_ex","commit_stats":null,"previous_names":["borodark/smc_ex"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/borodark/smc_ex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borodark%2Fsmc_ex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borodark%2Fsmc_ex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borodark%2Fsmc_ex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borodark%2Fsmc_ex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/borodark","download_url":"https://codeload.github.com/borodark/smc_ex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/borodark%2Fsmc_ex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34412784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bayesian-inference","beam","elixir","elixir-lang","elixir-library","epidemiology","particle-filter","pmcmc","probabilistic-programming","sequential-monte-carlo","state-estimation"],"created_at":"2026-06-16T15:30:31.084Z","updated_at":"2026-06-16T15:30:32.119Z","avatar_url":"https://github.com/borodark.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SMC-Ex\n\n### Translator's Foreword\n\nThe algorithms in this library belong to a lineage that begins with Gordon,\nSalmond, and Smith's bootstrap filter (1993) and runs through Andrieu, Doucet,\nand Holenstein's particle MCMC (2010) to Chopin, Jacob, and Papaspiliopoulos'\nSMC² (2013). The online variant that gives this library its centerpiece is due\nto Vieira (2018), who observed that evaluating the PMCMC acceptance ratio over\na fixed window of recent observations keeps the cost constant — an insight\nwhose practical importance for epidemic surveillance was demonstrated by\nTemfack and Wyse (2025).\n\nWhat we have done is translate these methods into Elixir, where the BEAM's\nlightweight process model turns the embarrassingly parallel structure of SMC²\ninto a one-liner: `Task.async_stream` over parameter particles, each running\nits own particle filter, supervised, fault-tolerant, and concurrent on as many\ncores as the machine provides. Any errors in the translation are the\ntranslator's own.\n\n---\n\n**Sequential Monte Carlo methods for Elixir.**\n\nParticle filters, PMCMC, and Online SMC². Pure Elixir — zero dependencies,\nzero NIFs, zero configuration. Install and run.\n\n![Beta convergence](assets/smc_beta_convergence.png)\n*Transmission rate β converging to truth (red dashed) as 80 daily case counts arrive. 200 θ-particles, pure Elixir, zero dependencies.*\n\n```elixir\n# Track an epidemic in real time — infer β, σ, γ as cases arrive\nresult = SMC.run(model, prior, daily_cases,\n  n_theta: 400, n_x: 200, window: 20, parallel: true)\n\nresult.posterior_history  # parameter estimates at each time step\n```\n\n## What's Inside\n\n| Module | Algorithm | Source | Use case |\n|---|---|---|---|\n| `SMC.ParticleFilter` | Bootstrap Particle Filter | Gordon et al. 1993 | State estimation with known parameters |\n| `SMC.PMCMC` | Particle Marginal MH | Andrieu et al. 2010 | Parameter inference via MH with PF likelihood |\n| `SMC.OnlineSMC2` | Online SMC² | Chopin et al. 2013, Vieira 2018 | Joint online parameter + state inference |\n\n## Installation\n\n```elixir\ndef deps do\n  [{:smc_ex, \"~\u003e 0.1\"}]\nend\n```\n\nZero dependencies. Compiles in seconds.\n\n## Quick Start: Track an Epidemic\n\n```elixir\n# 1. Define the state-space model (parameterized by θ)\nmodel = %{\n  init: fn theta, rng -\u003e\n    {%{s: 999, e: 1, i: 0, r: 0}, rng}\n  end,\n\n  transition: fn state, theta, _t, rng -\u003e\n    p_se = 1 - :math.exp(-theta.beta * state.i / 1000)\n    p_ei = 1 - :math.exp(-theta.sigma)\n    p_ir = 1 - :math.exp(-theta.gamma)\n\n    {u1, rng} = :rand.uniform_s(rng)\n    y_se = round(max(0, state.s * p_se + (u1 - 0.5) * :math.sqrt(state.s * p_se)))\n    y_ei = round(max(0, state.e * p_ei))\n    y_ir = round(max(0, state.i * p_ir))\n\n    new = %{s: state.s - y_se, e: state.e + y_se - y_ei,\n            i: state.i + y_ei - y_ir, r: state.r + y_ir}\n    {new, rng}\n  end,\n\n  observation_logp: fn state, theta, y_obs -\u003e\n    lambda = max(state.i * theta.sigma, 0.1)\n    y_obs * :math.log(lambda) - lambda\n  end\n}\n\n# 2. Define prior on parameters\nprior = %{\n  sample: fn rng -\u003e\n    {u1, rng} = :rand.uniform_s(rng)\n    {u2, rng} = :rand.uniform_s(rng)\n    {u3, rng} = :rand.uniform_s(rng)\n    {%{beta: u1, sigma: u2 * 0.5, gamma: u3 * 0.3}, rng}\n  end,\n  logpdf: fn theta -\u003e\n    if theta.beta \u003e 0 and theta.sigma \u003e 0 and theta.gamma \u003e 0,\n      do: 0.0, else: -1.0e30\n  end\n}\n\n# 3. Run — observations arrive sequentially\nresult = SMC.run(model, prior, daily_cases,\n  n_theta: 200,    # parameter particles\n  n_x: 100,        # state particles per θ\n  window: 20,      # O-SMC² window size\n  n_moves: 3,      # PMCMC moves per rejuvenation\n  parallel: true    # use all cores\n)\n\n# 4. Results\nresult.posterior_history  # [{%{beta: 0.38, sigma: 0.24, gamma: 0.16}, ...}, ...]\nresult.ess_history        # [200.0, 195.3, ..., 48.7, ...]  (drops trigger rejuv)\nresult.log_evidence       # -342.7 (model evidence for comparison)\nresult.rejuvenation_count # 5 (how many times PMCMC ran)\n```\n\n## Particle Filter Only (Known Parameters)\n\nWhen θ is known and you just need to track latent states:\n\n```elixir\nmodel = %{\n  init: fn rng -\u003e {%{x: 0.0}, rng} end,\n  transition: fn state, t, rng -\u003e\n    {noise, rng} = :rand.normal_s(rng)\n    {%{x: state.x + noise}, rng}\n  end,\n  observation_logp: fn state, y_t -\u003e\n    z = (y_t - state.x) / 0.5\n    -0.5 * z * z\n  end\n}\n\nresult = SMC.filter(model, observations, n_particles: 500)\n\nresult.filtering_means  # weighted mean state at each time step\nresult.log_evidence     # log marginal likelihood\nresult.ess_history      # particle filter ESS\n```\n\n## The BEAM Advantage\n\nO-SMC² has two embarrassingly parallel loops:\n\n1. **BPF step** for each θ-particle — propagate states, weight by likelihood\n2. **PMCMC rejuvenation** — propose new θ, evaluate windowed likelihood\n\nOn the BEAM, both are `Task.async_stream`:\n\n```elixir\ntheta_particles\n|\u003e Task.async_stream(\u0026bpf_step/1, max_concurrency: System.schedulers_online())\n```\n\n**Fault tolerance for free**: if one particle's filter crashes (numerical\ndegeneracy, extreme parameter proposal), the `Task` catches the `:exit` — all\nother particles continue unaffected. This is fault-tolerant particle filtering\nby construction. In Python, one particle crashing kills the entire run.\n\n**Scaling**: On an 88-core server, 400 θ-particles run in ~5 waves (400/88).\nEach wave processes one observation's worth of BPF steps in parallel.\n\n## Options\n\n### SMC.run (O-SMC²)\n\n| Option | Default | Description |\n|---|---|---|\n| `n_theta` | 200 | Parameter particles |\n| `n_x` | 100 | State particles per θ-particle |\n| `window` | 20 | Window size for O-SMC² (tk). Larger = more accurate, slower |\n| `resample_threshold` | 0.5 | ESS fraction triggering rejuvenation |\n| `n_moves` | 3 | PMCMC moves per rejuvenation |\n| `proposal_scale` | 2.0 | Scale factor for Normal proposal covariance |\n| `parallel` | true | Use `Task.async_stream` |\n| `seed` | 42 | Random seed |\n\n### SMC.filter (Particle Filter)\n\n| Option | Default | Description |\n|---|---|---|\n| `n_particles` | 200 | Number of particles |\n| `resample_threshold` | 0.5 | ESS fraction triggering resampling |\n| `seed` | 42 | Random seed |\n\n## Model Specification\n\n### For O-SMC² (parameters unknown)\n\n```elixir\nmodel = %{\n  init: fn(theta, rng) -\u003e {initial_state, rng},\n  transition: fn(state, theta, t, rng) -\u003e {new_state, rng},\n  observation_logp: fn(state, theta, y_t) -\u003e log_likelihood\n}\n\nprior = %{\n  sample: fn(rng) -\u003e {theta_map, rng},\n  logpdf: fn(theta) -\u003e log_prior_density\n}\n```\n\n### For Particle Filter (parameters known)\n\n```elixir\nmodel = %{\n  init: fn(rng) -\u003e {initial_state, rng},\n  transition: fn(state, t, rng) -\u003e {new_state, rng},\n  observation_logp: fn(state, y_t) -\u003e log_likelihood\n}\n```\n\nStates and parameters are plain Elixir maps. No tensors, no special types.\n\n## When to Use What\n\n| Your situation | Method | Library |\n|---|---|---|\n| Known parametric model, continuous parameters | NUTS / HMC | eXMC, Stan, PyMC |\n| Unknown functional form, many features | BART | StochTree-Ex |\n| **Discrete state transitions, unknown parameters, streaming** | **O-SMC²** | **smc_ex** |\n| Known parameters, tracking latent states | Particle filter | smc_ex |\n| Epidemic surveillance in real time | O-SMC² | smc_ex |\n| Hidden Markov Models | Particle filter + EM or O-SMC² | smc_ex |\n\n## Notebooks\n\n- `notebooks/01_epidemic_tracking.livemd` — Full SEIR epidemic tracking with\n  O-SMC², synthetic data, VegaLite charts\n\n## Architecture\n\n```\nSMC.run(model, prior, observations)\n  │\n  ├── Initialize Nθ parameter particles from prior\n  │\n  └── For each observation y_t:\n      │\n      ├── For each θ-particle (parallel):\n      │   └── SMC.ParticleFilter: one BPF step\n      │       → incremental likelihood p̂(y_t | θ)\n      │\n      ├── Reweight θ-particles\n      │\n      └── If ESS \u003c threshold:\n          ├── Resample θ-particles\n          └── For each θ-particle (parallel):\n              └── SMC.PMCMC: M Metropolis-Hastings moves\n                  └── SMC.ParticleFilter.filter_window\n                      (only last tk observations — constant cost)\n```\n\n## References\n\n- Gordon, N., Salmond, D. \u0026 Smith, A. (1993). \"Novel approach to nonlinear/\n  non-Gaussian Bayesian state estimation.\" *IEE Proceedings F*.\n- Andrieu, C., Doucet, A. \u0026 Holenstein, R. (2010). \"Particle Markov chain\n  Monte Carlo methods.\" *JRSS-B*, 72, 1-269.\n- Chopin, N., Jacob, P.E. \u0026 Papaspiliopoulos, O. (2013). \"SMC²: An efficient\n  algorithm for sequential analysis of state space models.\" *JRSS-B*, 75, 397-426.\n- Vieira, R.M. (2018). *Bayesian Online State and Parameter Estimation for\n  Streaming Data*. PhD thesis, Newcastle University.\n- Temfack, D. \u0026 Wyse, J. (2025). \"Sequential Monte Carlo Squared for online\n  inference in stochastic epidemic models.\" *Epidemics* 52, 100847.\n\n\n## The Ecosystem: _Three Comrades_\n\n_Probabiliers de tous les a priori, unissez-vous!_\n\nsmc_ex is one of three standalone Elixir libraries for Bayesian inference.\nDifferent algorithms, different use cases, zero shared dependencies.\n\n| Library | Algorithm | For |\n|---|---|---|\n| [**eXMC**](https://github.com/borodark/eXMC) | NUTS / HMC | Known parametric models, continuous parameters |\n| **smc_ex** | Bootstrap PF, PMCMC, Online SMC² | Discrete states, streaming data, epidemic tracking |\n| [**StochTree-Ex**](https://github.com/borodark/ex_stochtree) | BART | Unknown functional form, feature discovery |\n\nThey compose in the same application — each is a Mix dependency:\n\n```elixir\ndef deps do\n  [\n    {:exmc, \"~\u003e 0.2\"},            # NUTS for continuous models\n    {:smc_ex, \"~\u003e 0.1\"},          # O-SMC² for discrete state-space\n    {:stochtree_ex, \"~\u003e 0.1\"},    # BART for nonparametric regression\n  ]\nend\n```\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborodark%2Fsmc_ex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fborodark%2Fsmc_ex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fborodark%2Fsmc_ex/lists"}