-
A MacOSv26/Xcode/Swift based CLI (Command Line Interface) to Stan's cmdstan executable.
-
Hosting a port of McElreath's R implementation ulam() in rethinking to Swift.
This project is work in progress!!! Work completed or still to be done can be found in TODO. Some additional technical details can be found in CLAUDE. Lots of testing and more examples are needed for the ulam pipeline.
| -------------------- | --------------------------=------------------------------ |
| Command | Effect |
| -------------------- | --------------------------------------------------------- |
| compile | Compile a Stan model |
| sample | Sample from a compiled model |
| stansummary | Stansummary on a sampled model |
| optimize | Optimize a compiled model |
| pathfinder | Pathfinder on a compiled model |
| laplace | Laplace on a compiled model |
| generated_quantities | Run generate_quantities on existing draws (see note 2) |
| runinfo | See note 1 |
| -------------------- | --------------------------------------------------------- |
Notes
- The
runinfocommand readsResults/<name>.config.json(written bysample, which renames cmdstan's<name>_output_config.json) and cleans it in place — stripping absolute paths to basenames and sorting keys. It is also used internally bystansummaryto determine the number of chains created bysample. - The
generated_quantitiescommand runs cmdstan's standalonegenerate_quantitiesmethod over the draws from a priorsamplerun and writesResults/<name>.generated_quantities.csv. The.stanfile must contain agenerated quantitiesblock — add asim()line to the alist before runningstancode(see UlamManual.md §2.3.1), or hand-edit the.standirectly.
| ------------ | -------------------------------------- |
| Command | Effect |
| ------------ | -------------------------------------- |
| ulam | Run the ulam pipeline end-to-end |
| stancode | alist -> .stan |
| stan2alist | .stan -> alist (inverse of stancode) |
| alist2dsl | alist -> smoke driver |
| dsl2stan | smoke driver -> .stan (swiftc) |
| ------------ | -------------------------------------- |
Notes
- By default
ulamprefers the fast in-processstancodepath when an ".alist.R" is present. - Command
ulamfalls back todsl2stanagainst a hand-authored "smoke driver". See DSLManual.md. - A
sim()line in an alist (y_rep <- sim(dnorm(mu, sigma))) causesstancodeto emit agenerated quantitiesblock. Rungenerated_quantitiesaftersampleto produce posterior-predictive draws. See UlamManual.md §2.3.1.
| ------------ | -------------------------------------- |
| Command | Effect |
| ------------ | -------------------------------------- |
| csv2json | "<name>.csv" -> "<name>.json" |
| stancases | Show or update the cases subdirectory |
| ------------ | -------------------------------------- |
Notes
- As with building a Stan binary during
compile, all commands only operate when the input file's modification timestamp is newer than the corresponding output file timestamp. - Run
csv2jsonpreferably after a ".stan" file has been set up. In that case the ".data.json" file reflects what is needed. It also adds items like 'N', the number of observations, and other needed data items such as 'N_blocks'.
This repository is an Xcode project. Some familiarity with running Xcode and Swift programs on MacOS is assumed. To edit documentation files, if not from within Xcode, I use Clearly.
To get going, start Xcode and:
- Click on
'Integrate'. - Select
'Clone'. - Enter the http address for this repository: "https://github.com/SwiftProjectOrganization/SwiftStan".
- Click
'Clone'.
The repository will be downloaded and the project will open in Xcode.
-
To use Stan's cmdstan, typically an environment variable
"CMDSTAN"is defined to point to the cmdstan directory. See references 1 and 2 below on how to install cmdstan and below .zshrc fragment how it can be included. -
'build and run'the project. -
Expand your CMDSTAN definition in your .zshrc with an
'alias'and the'SWIFTSTAN_PROJECT_ROOT'environment variable:
export CMDSTAN=/Users/rob/Projects/StanSupport/cmdstan/
launchctl setenv CMDSTAN /Users/rob/Projects/StanSupport/cmdstan/
alias swiftstan="/Users/rob/Library/Developer/Xcode/DerivedData/SwiftStan-*/Build/Products/Debug/swiftstan"
export SWIFTSTAN_PROJECT_ROOT="/Users/rob/Projects/Swift/SwiftStan"
launchctl setenv SWIFTSTAN_PROJECT_ROOT /Users/rob/Projects/Swift/SwiftStan
Make sure "swiftstan" points to the most recent version of SwiftStan in "../Xcode/DerivedData"
Environment variables used by the pipeline:
CMDSTAN— location of the cmdstan installation (required forcompile/sample).`SWIFTSTAN_PROJECT_ROOT— location of the SwiftStan source checkout, used by the DSL pipeline'sdsl2stanto compile a "smoke driver". It defaults to/Users/rob/Projects/Swift/SwiftStananddsl2stanprints a notice that the default is being used.
After finishing the setup steps, in a (MacOS or other) Terminal:
-
Navigate to the SwiftStan directory, e.g.
cd ~/Project/Swift/SwiftStan. -
Enter
swift test.
To run individual tests, use for example swift test --filter "chimpanzeesHappyPath()".
After the initial build, the intended usage is to run the commands from a shell. This requires the exported alias in a shell as setup above. It's not always necessary, but advisable, to run swiftstan ... from the SwiftStan directory.
The above commands can also be run from within Xcode by specifying input arguments before hitting the 'build and run' button. See below "Usage from within Xcode".
All pipeline commands operate on a set of files stored in the directory "~/Documents/<STAN_CASES>/<name>/...". Here is the name of a model, e.g. bernoulli or chimpanzees.
The case-root directory defaults to 'StanCases'. Use swiftstan stancases <name> to set a different root (persisted across invocations).
The cmdstan pipeline only uses files in "~/Documents/<STAN_CASES>/name>/Results". All cmdstan output files also end up in Results.
The ulam pipeline looks either for files in "~/Documents/<STAN_CASES>/<name>/Preliminaries", or, in case of the command stan2alist, for a "<name>.stan" file in "<name>/Results".
Produced files end up in either Preliminaries ("<Name>.ulam.swift" and "<name>.alist.r") or in Results ("<name>.stan" and "<name>.data.json").
In "~/Documents/<STAN_CASES>/<name>/Preliminaries" 3 files can be present:
1. `"<name>.csv"`: A .csv file containing the data for the <name>.
2. `"<name>.alist.r"`: A .r fragment containing an R alist as used in `'rethinking'`.
3. `"<Name>.ulam.swift"`: Intermediate file for debugging or handcoding Ulam DSL.
In "~/Documents/<STAN_CASES>/<name>/Results" at least 2 files must be present before the cmdstan pipeline can be used:
1. `"<name>.data.json"`: If data is needed for the model.
2. `"<name>.stan"`: Stan language program.
See the examples in UlamManual.md and DSLManual.md for details.
Every cmdstan call (compile, sample, optimize, laplace, pathfinder, stansummary) writes its raw stdout and stderr to the model's Results/ directory as:
~/Documents/<STAN_CASES>/name>/Results/<name>.<method>.log # captured stdout
~/Documents/<STAN_CASES>/name>/Results/<name>.<method>.error.log # captured stderr
Both files are written on every run (zero bytes means "ran but emitted nothing"); each invocation overwrites the previous log. cmdstan emits most diagnostics (warmup banners, divergence messages, treedepth warnings) to stdout, so the .log file is normally where to look first; .error.log is reserved for hard failures and a few compile-time messages.
The sample command uses save_cmdstan_config=true by default. cmdstan writes <name>_output_config.json; sample renames it to <name>.config.json. The runinfo subcommand reads that file and cleans it in place (absolute paths → basenames, sorted keys).
The repository ships ready-to-run inputs for every model worked through in the two manuals under the top-level Examples/ directory. Each example is a self-contained case directory named after the model:
Examples/<name>/
├── Preliminaries/ the inputs (<name>.csv, <name>.alist.R, and for the DSL cases <Name>.ulam.swift)
└── Results/ empty — the pipeline writes its output here
To follow an example in a manual, copy the case directory into your ~/Documents/<STAN_CASES>/ root and run the pipeline against it. For example:
cp -R Examples/SR2Cases/howell_m4_4 ~/Documents/SR2Cases/
swiftstan stancases SR2Cases
swiftstan ulam --model howell_m4_4The cases are in the Examples/StanCases/<name>/ and Examples/SR2Cases/<name>/ directories of the repository.
Each Results/ ships mostly empty (it carries only a .gitkeep placeholder); the pipeline populates it on the first run. The exception is radon_pp_template, whose Results/ ships the hand-written .stan that stan2alist reads.
The package can be used from the CLI (Terminal) or from within Xcode.
Help is available with 'swiftstan -h' or 'swiftstan compile -h'.
rob@Rob-Travel-M5 ~ % swiftstan -h
OVERVIEW: A wrapper for running cmdstan.
USAGE: swiftstan <subcommand>
OPTIONS:
--version Show the version.
-h, --help Show help information.
SUBCOMMANDS:
compile Compile the Stan model.
sample Sample the Stan model.
optimize Optimize the Stan model.
pathfinder Use Pathfinder approximation.
laplace Run cmdstan's Laplace approximation on a compiled model.
stansummary Run the Stan summary program.
csv2json Read Preliminaries/<name>.csv, writes Results/<name>.data.json.
dsl2stan Compile a Preliminaries/*.ulam.swift, write Results/<name>.stan.
alist2dsl Translate Preliminaries/<name>.alist.R into Preliminaries/<Name>.ulam.swift.
stancode Translate Preliminaries/<name>.alist.R straight to Results/<name>.stan (in-process, no swiftc).
stan2alist Reverse-translate Results/<name>.stan into Preliminaries/<name>.alist.R (inverse of stancode).
runinfo Clean Results/<name>.config.json in place (basenames, sorted keys).
ulam Run one of the built-in ulam DSL demos (--model Bernoulli|Poisson|Binomial|UCB|Dmvnorm).
stancases Show or set the <Stan_Cases> directory, `swiftstan stancases SR2Cases`.
test (default) Test the CLI functions.
See 'swiftstan help <subcommand>' for detailed help.
rob@Rob-Travel-M5 ~ %
If the appropriate files are present, a typical Terminal session could continue with:
'swiftstan compile --model bernoulli''swiftstan sample --model bernoulli'
or
'swiftstan ulam --model chimpanzees'
The alist2dsl command uses Swift to first create an intermediate 'DSL smoke driver' which takes roughly 6 seconds longer. But I do like the option to generate the DSL where the structure of the Stan model is clearly labeled and the input data file checked.
Edit the schema arguments, e.g. 'compile --model=bernoulli' and press "build-and-run".
Use the -I switch to install required files to compile and sample a bernoulli Stan Language Program.
In the SwiftStan directory:
swiftstan compile -Iswiftstan sample -I
These are the R alist names that stancode / alist2dsl recognise. DSL-only distributions (not accessible from an .alist.R file) are noted in the second column.
| Alist (R) name | Stan sampling name | DSL node | Notes |
|---|---|---|---|
dnorm(mu, sigma) |
normal |
Prior / Likelihood |
|
dbinom(1, p) |
bernoulli |
Likelihood |
McElreath shorthand; collapses in lowering |
dbern(p) |
bernoulli |
Likelihood |
Direct 1-arg form |
dbinom(n, p) |
binomial |
Likelihood |
General case |
dbeta(a, b) |
beta |
Prior |
|
dexp(r) |
exponential |
Prior |
|
dpois(r) |
poisson |
Likelihood |
|
dgamma(shape, rate) |
gamma |
Prior |
|
dcauchy(mu, sigma) |
cauchy |
Prior |
|
dlnorm(mu, sigma) |
lognormal |
Prior |
|
dunif(lower, upper) |
uniform |
Prior |
|
dt(nu, mu, sigma) |
student_t |
Prior |
|
dmvnorm(mu, sigma) |
multi_normal |
Likelihood |
SUR only |
dlkjcorr(eta) |
lkj_corr_cholesky |
LKJCorrCholeskyPrior |
Grouped-indexed; maps to Cholesky form |
dmvnormchol(Mu, L_Rho, sigma) |
multi_normal_cholesky |
VaryingVectorPrior |
Grouped-indexed only |
dmvnorm2(Mu, sigma, Rho) |
multi_normal_cholesky |
VaryingVectorPrior |
Grouped-indexed only; arg order differs from dmvnormchol |
| — (DSL only) | wishart |
WishartPrior |
No alist name |
| — (DSL only) | ordered_logistic |
Likelihood + OrderedCutpoints |
No alist name |
| — (DSL only) | ordered_probit |
Likelihood + OrderedCutpoints |
No alist name |
| — (DSL only) | dirichlet |
Prior |
No alist name; used with SimplexPrior |
Sim("y_rep", .dist(...)) in the DSL (written y_rep <- sim(d*(...)) in an .alist.R) emits a generated quantities block entry of the form array[N] int/real y_rep = dist_rng(args);. The _rng name is the Stan sampling name with _rng appended; the output type is array[N] int for discrete distributions and array[N] real for continuous ones.
Supported — Stan has a scalar-returning _rng function:
| Distribution | Alist sim() |
DSL Sim() |
Stan emitted | Output type |
|---|---|---|---|---|
normal |
sim(dnorm(mu, sigma)) |
.normal(mu, sigma) |
normal_rng(mu, sigma) |
array[N] real |
bernoulli |
sim(dbinom(1, p)) |
.bernoulli(p: p) |
bernoulli_rng(p) |
array[N] int |
binomial |
sim(dbinom(n, p)) |
.binomial(n: n, p: p) |
binomial_rng(n, p) |
array[N] int |
beta |
sim(dbeta(a, b)) |
.beta(a, b) |
beta_rng(a, b) |
array[N] real |
exponential |
sim(dexp(r)) |
.exponential(r) |
exponential_rng(r) |
array[N] real |
poisson |
sim(dpois(r)) |
.poisson(r) |
poisson_rng(r) |
array[N] int |
gamma |
sim(dgamma(shape, rate)) |
.gamma(shape, rate) |
gamma_rng(shape, rate) |
array[N] real |
cauchy |
sim(dcauchy(mu, sigma)) |
.cauchy(mu, sigma) |
cauchy_rng(mu, sigma) |
array[N] real |
lognormal |
sim(dlnorm(mu, sigma)) |
.lognormal(mu, sigma) |
lognormal_rng(mu, sigma) |
array[N] real |
uniform |
sim(dunif(a, b)) |
.uniform(lower: a, upper: b) |
uniform_rng(a, b) |
array[N] real |
student_t |
sim(dt(nu, mu, sigma)) |
.studentT(nu: nu, mu: mu, sigma: sigma) |
student_t_rng(nu, mu, sigma) |
array[N] real |
ordered_logistic |
— (DSL only) | .orderedLogistic(eta: eta, cutpoints: c) |
ordered_logistic_rng(eta, c) |
array[N] int |
Not supported in Sim() — no scalar Stan _rng or wrong return type:
| Distribution | Reason |
|---|---|
multi_normal |
multi_normal_rng returns a vector, not a scalar — array[N] real declaration is wrong |
multi_normal_cholesky |
multi_normal_cholesky_rng returns a vector — same issue |
lkj_corr_cholesky |
lkj_corr_cholesky_rng does not exist in Stan's function library |
wishart |
wishart_rng returns a matrix — array[N] real is wrong |
dirichlet |
dirichlet_rng returns a vector (simplex) — array[N] real is wrong |
ordered_probit |
ordered_probit_rng does not exist in Stan's function library |
Using any of the unsupported distributions with Sim() throws DataInferenceError.unsupportedSimDistribution at code-generation time — before any Stan is emitted.
../CLAUDE.md— architecture notes for the SwiftStan package (Commands / Methods / Support layering, the(String, String)return convention, the Ulam module layout, etc.). Loaded by Claude Code sessions in this workspace.TODO.md— forward-looking punch list for more advanced topics (SUR, LKJ-Cholesky, post-sampling helpers, etc.).
- Stan
- cmdstan
- [McElreath, Statistical Rethinking (2nd ed.)}(https://www.routledge.com/Statistical-Rethinking-A-Bayesian-Course-with-Examples-in-R-and-STAN/McElreath/p/book/9780367139919) —
ulam()is from the accompanying Rrethinkingpackage.