diff --git a/.github/workflows/ghp-deploy.yml b/.github/workflows/ghp-deploy.yml
new file mode 100644
index 000000000..42a31bb72
--- /dev/null
+++ b/.github/workflows/ghp-deploy.yml
@@ -0,0 +1,79 @@
+name: Build and Deploy Jekyll Site to GitHub Pages
+
+on:
+ push:
+ branches: [ master, main ]
+ pull_request:
+ branches: [ master, main ]
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Build job
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Not needed if lastmod is not enabled
+
+ # Setup Python
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.x'
+
+ # Generate Stan index files
+ - name: Generate Stan index files
+ run: python ./ghp-deployment/generate_index_files_for_ghp_deploy.py
+
+ # Setup Ruby
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.1' # Not needed with a .ruby-version file
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
+ cache-version: 0 # Increment this number if you need to re-download cached gems
+
+ # Setup Pages
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v4
+
+ # Build with Jekyll
+ - name: Build with Jekyll
+ # Outputs to the './_site' directory by default
+ run: bundle exec jekyll build --baseurl "example-models"
+ env:
+ JEKYLL_ENV: production
+ PAGES_REPO_NWO: magland/example-models
+
+ # Upload artifact
+ - name: Upload artifact
+ # Automatically uploads an artifact from the './_site' directory by default
+ uses: actions/upload-pages-artifact@v3
+
+ # Deployment job
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ # Deploy only on pushes to master/main branch, not on pull requests
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index e4510159d..097b1992f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@
story_cache/
story_files/
.DS_Store
+_site
+Gemfile.lock
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 000000000..072044272
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+gem "github-pages", group: :jekyll_plugins
+# Ruby ≥3.0 needs this to run `jekyll serve`
+gem "webrick", "~> 1.8"
\ No newline at end of file
diff --git a/README.md b/README.md
index 70ff2abd9..a99516d5b 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
This repository holds open source Stan models, data simulators, and real data. There are models translating those found in books, most of the BUGS examples, and some basic examples used in the manual.
+**[Open Interactive Browser](https://magland.github.io/example-models)** - View, run, and/or edit these Stan examples directly in your browser using Stan Playground in embedded mode.
+
#### Books
* [Applied Regression Modeling](https://github.com/stan-dev/example-models/wiki/ARM-Models) (Gelman and Hill 2007)
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 000000000..9b7219c00
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1 @@
+url: https://magland.github.io
\ No newline at end of file
diff --git a/ghp-deployment/README.md b/ghp-deployment/README.md
new file mode 100644
index 000000000..62219f2b2
--- /dev/null
+++ b/ghp-deployment/README.md
@@ -0,0 +1,31 @@
+# GitHub Pages Deployment System
+
+This directory contains tools for the automated deployment of example-models to GitHub Pages with interactive Stan Playground embeds.
+
+## Overview
+
+The `generate_index_files_for_ghp_deploy.py` script automatically generates `index.md` files during the GitHub Actions workflow. It has two main functions:
+
+1. For directories containing `.stan` files:
+ - Creates index files with Stan Playground embed code
+ - Includes any corresponding `.data.json` file if present (optional)
+ - Enables interactive model viewing/editing directly in the browser
+ - Stan files without `.data.json` still get embedded with empty data
+
+2. For other directories:
+ - Creates simple index files with links to subdirectories and files
+ - Maintains navigation structure through the repository
+
+## How it Works
+
+The script is executed automatically by GitHub Actions when changes are pushed to the repository:
+
+1. Recursively processes all directories
+2. For each directory:
+ - Finds all `.stan` files and optional `.data.json` pairs
+ - Generates appropriate index.md content:
+ - Stan files get interactive playground embeds (with or without data)
+ - Other directories get file/folder navigation links
+ - Creates or updates the index.md file
+
+This automated process enables the interactive browser experience at https://magland.github.io/example-models
diff --git a/ghp-deployment/generate_index_files_for_ghp_deploy.py b/ghp-deployment/generate_index_files_for_ghp_deploy.py
new file mode 100755
index 000000000..2f7434016
--- /dev/null
+++ b/ghp-deployment/generate_index_files_for_ghp_deploy.py
@@ -0,0 +1,298 @@
+#!/usr/bin/env python3
+"""
+Script to automatically generate index.md files for all directories.
+
+This script recursively searches through the project and creates index.md files
+for all directories. For directories containing .stan files (with optional .data.json files),
+it creates files with stan-playground embed code for viewing the models in Stan
+Playground. Stan models without .data.json files are embedded with empty data.
+For other directories, it creates simple index files with links to all subdirectories
+and files. Always overwrites existing index.md files.
+"""
+
+from pathlib import Path
+
+
+def verify_readme():
+ """
+ Verify that README.md exists in the root directory and contains the exact text "## Example Models".
+
+ Returns:
+ bool: True if verification passes, False otherwise
+ """
+ readme_path = Path("README.md")
+ if not readme_path.exists():
+ print("Error: README.md not found in the root directory.")
+ return False
+
+ try:
+ with open(readme_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+ if not content.startswith("## Example Models"):
+ print("Error: README.md does not start with '## Example Models'.")
+ print("This script should only be run in the example-models repository.")
+ return False
+ return True
+ except Exception as e:
+ print(f"Error reading README.md: {e}")
+ return False
+
+def find_stan_files_and_data(directory):
+ """
+ Find all .stan files and their optional .data.json files in a directory.
+ All .stan files will be included in the result, with or without corresponding data files.
+
+ Args:
+ directory (Path): Directory to search in
+
+ Returns:
+ list: List of tuples (stan_file, data_file) where data_file is None if no corresponding .data.json exists
+ """
+ pairs = []
+ stan_files = list(directory.glob("*.stan"))
+
+ for stan_file in stan_files:
+ # Get the base name without extension
+ base_name = stan_file.stem
+ # Look for corresponding .data.json file
+ data_file = directory / f"{base_name}.data.json"
+
+ if data_file.exists():
+ pairs.append((stan_file.name, data_file.name))
+ else:
+ # Include .stan file even without corresponding .data.json
+ pairs.append((stan_file.name, None))
+
+ return pairs
+
+
+def has_stan_files_recursive(directory):
+ """
+ Recursively check if a directory or any of its subdirectories contains .stan files.
+
+ Args:
+ directory (Path): Directory to search in
+
+ Returns:
+ bool: True if any .stan files are found in the directory tree, False otherwise
+ """
+ # Check if the current directory has stan files
+ if find_stan_files_and_data(directory):
+ return True
+
+ # Recursively check all subdirectories
+ for item in directory.iterdir():
+ if item.is_dir() and not item.name.startswith('.'):
+ # Skip common build/cache directories
+ if item.name not in ['node_modules', '__pycache__', '.git', '_site']:
+ if has_stan_files_recursive(item):
+ return True
+
+ return False
+
+
+def generate_directory_content(directory):
+ """
+ Generate the content for an index.md file for directories without Stan pairs.
+
+ Args:
+ directory (Path): Directory containing the files
+
+ Returns:
+ str: Generated index.md content
+ """
+ content = f"# {directory.name}\n\n"
+
+ # Get all subdirectories and categorize them
+ subdirs_with_stan = []
+ subdirs_other = []
+
+ for item in directory.iterdir():
+ if item.is_dir() and not item.name.startswith('.'):
+ if item.name not in ['node_modules', '__pycache__', '.git', '_site']:
+ # Check if this subdirectory contains .stan/.data.json pairs recursively
+ subdir_path = directory / item.name
+ if has_stan_files_recursive(subdir_path):
+ subdirs_with_stan.append(item.name)
+ else:
+ subdirs_other.append(item.name)
+
+ # Add subdirectories with Stan files section
+ if subdirs_with_stan:
+ subdirs_with_stan.sort()
+ content += "## Subdirectories with Stan Files\n\n"
+ for subdir in subdirs_with_stan:
+ content += f"- [{subdir}](./{subdir}/)\n"
+ content += "\n"
+
+ # Add other subdirectories section
+ if subdirs_other:
+ subdirs_other.sort()
+ content += "## Other Subdirectories\n\n"
+ for subdir in subdirs_other:
+ content += f"- [{subdir}](./{subdir}/)\n"
+ content += "\n"
+
+ # Get all files in the directory, excluding index.md itself
+ all_files = []
+ for file_path in directory.iterdir():
+ if file_path.is_file() and file_path.name != 'index.md':
+ all_files.append(file_path.name)
+
+ if all_files:
+ all_files.sort()
+ content += "## Files\n\n"
+ for filename in all_files:
+ content += f"- [{filename}](./{filename})\n"
+
+ return content
+
+
+def generate_index_content(pairs, directory):
+ """
+ Generate the content for an index.md file based on stan files and their optional data files.
+ Stan files without corresponding .data.json files will be embedded with empty data.
+
+ Args:
+ pairs (list): List of tuples (stan_file, data_file) where data_file is None for files without data
+ directory (Path): Directory containing the files
+
+ Returns:
+ str: Generated index.md content
+ """
+ content = '\n\n'
+
+ for stan_file, data_file in pairs:
+ # Add heading with the filename including .stan extension
+ base_name = Path(stan_file).stem
+ content += f'## {base_name}.stan\n\n'
+
+ content += f'\n'
+ content += f'\n'
+ content += f'\n'
+
+ # Add spacing between multiple embeds
+ if len(pairs) > 1 and (stan_file, data_file) != pairs[-1]:
+ content += '\n'
+
+ # Add links to all files in the directory
+ content += '\n## Files\n\n'
+
+ # Get all files in the directory, excluding index.md itself
+ all_files = []
+ for file_path in directory.iterdir():
+ if file_path.is_file() and file_path.name != 'index.md':
+ all_files.append(file_path.name)
+
+ # Sort files for consistent ordering
+ all_files.sort()
+
+ for filename in all_files:
+ content += f'- [{filename}](./{filename})\n'
+
+ return content
+
+
+def process_directory(directory, stats):
+ """
+ Process a single directory and create index.md file.
+
+ Args:
+ directory (Path): Directory to process
+ stats (dict): Statistics dictionary to update
+ """
+ pairs = find_stan_files_and_data(directory)
+ index_path = directory / "index.md"
+
+ # Determine if this index.md existed before
+ existed_before = index_path.exists()
+
+ if pairs:
+ print(f"Found {len(pairs)} Stan/data pair(s) in {directory}")
+ for stan_file, data_file in pairs:
+ print(f" - {stan_file} + {data_file}")
+
+ # Generate content with Stan playground embeds
+ content = generate_index_content(pairs, directory)
+ else:
+ # Generate content for directory without Stan pairs
+ content = generate_directory_content(directory)
+
+ # Always write the index.md file (overwrite if exists)
+ try:
+ with open(index_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ if existed_before:
+ print(f" ✓ Updated {index_path}")
+ stats['updated'] += 1
+ else:
+ print(f" ✓ Created {index_path}")
+ stats['created'] += 1
+
+ except Exception as e:
+ print(f" ✗ Error writing {index_path}: {e}")
+ stats['errors'] += 1
+
+
+def main():
+ """Main function to run the script."""
+ print("Stan Index Generator")
+ print("===================")
+
+ # Verify we're in the correct repository
+ if not verify_readme():
+ return
+
+ print("Creating index.md files for all directories...")
+ print()
+
+ # Start from the current working directory
+ root_dir = Path(".")
+
+ # Statistics tracking
+ stats = {
+ 'created': 0,
+ 'updated': 0,
+ 'errors': 0,
+ 'directories_scanned': 0
+ }
+
+ # Process the root directory first
+ stats['directories_scanned'] += 1
+ process_directory(root_dir, stats)
+
+ # Recursively walk through all directories
+ for current_dir in root_dir.rglob("*"):
+ if current_dir.is_dir():
+ # Skip hidden directories and common build/cache directories
+ if any(part.startswith('.') for part in current_dir.parts):
+ continue
+ if any(part in ['node_modules', '__pycache__', '.git', '_site'] for part in current_dir.parts):
+ continue
+
+ stats['directories_scanned'] += 1
+ process_directory(current_dir, stats)
+
+ # Print summary
+ print()
+ print("Summary")
+ print("=======")
+ print(f"Directories scanned: {stats['directories_scanned']}")
+ print(f"Index files created: {stats['created']}")
+ print(f"Index files updated: {stats['updated']}")
+ print(f"Errors encountered: {stats['errors']}")
+
+ if stats['created'] + stats['updated'] > 0:
+ print(f"\n✓ Successfully processed {stats['created'] + stats['updated']} directories")
+ else:
+ print("\nNo directories found to process.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/knitr/car-iar-poisson/icar_stan.Rmd b/knitr/car-iar-poisson/icar_stan.Rmd
index 263e9b906..6162bd762 100644
--- a/knitr/car-iar-poisson/icar_stan.Rmd
+++ b/knitr/car-iar-poisson/icar_stan.Rmd
@@ -147,7 +147,7 @@ the BYM model.
The corresponding conditional distribution specification is:
-$$ p \left( { \phi }_i \, \vert\, {\phi}_j \, j \neq i, {{\tau}_i}^{-1} \right)
+$$ p \left( { \phi }_i \, \vert\, {\phi}_j \, j \neq i, { {\tau}_i}^{-1} \right)
= \mathit{N} \left( \frac{\sum_{i \sim j} {\phi}_{i}}{d_{i,i}}, \frac{1}{d_{i,i} {\tau}_i} \right)$$
where $d_{i,i}$ is the number of neighbors for region $n_i$.
@@ -194,16 +194,16 @@ $$
\log p(\phi) &= -{\frac{1}{2}} {\phi}^T [D - W] \phi + \mbox{const} \\
&= -{\frac{1}{2}} \left( \sum_{i,j} {\phi}_i {[D - W]}_{i,j} {\phi}_j \right) + \mbox{const} \\
&= -{\frac{1}{2}} \left( \sum_{i,j} {\phi}_i\,{\phi}_j D_{i,j} - \sum_{i,j} {\phi}_i\,{\phi}_j W_{i,j} \right) + \mbox{const} \\
-&= -{\frac{1}{2}} \left( \sum_{i} {{\phi}_i}^2\,D_{i,i} - \sum_{i \sim j} 2\ {\phi}_i\,{\phi}_j \right) + \mbox{const} \\
-&= -{\frac{1}{2}} \left( \sum_{i \sim j} ({{\phi}_i}^2 + {{\phi}_j}^2) - \sum_{i \sim j} 2\ {\phi}_i\,{\phi}_j \right) + \mbox{const} \\
-&= -{\frac{1}{2}} \left( \sum_{i \sim j} {{\phi}_i}^2 - 2\ {\phi}_i\,{\phi}_j + {{\phi}_j}^2 \right) + \mbox{const} \\
+&= -{\frac{1}{2}} \left( \sum_{i} { {\phi}_i}^2\,D_{i,i} - \sum_{i \sim j} 2\ {\phi}_i\,{\phi}_j \right) + \mbox{const} \\
+&= -{\frac{1}{2}} \left( \sum_{i \sim j} ({ {\phi}_i}^2 + { {\phi}_j}^2) - \sum_{i \sim j} 2\ {\phi}_i\,{\phi}_j \right) + \mbox{const} \\
+&= -{\frac{1}{2}} \left( \sum_{i \sim j} { {\phi}_i}^2 - 2\ {\phi}_i\,{\phi}_j + { {\phi}_j}^2 \right) + \mbox{const} \\
&= -{\frac{1}{2}} \left( \sum_{i \sim j} {({\phi}_i - {\phi}_j)}^2 \right) + \mbox{const}
\end{align}
$$
Since $D$ is the diagonal matrix where $D_{i,i}$ is the number of neighbors and
the off-diagonal entries have value $0$.
-The expression $\sum_{i,j} {\phi}_i\,{\phi}_j D_{i,j}$ rewrites to terms ${{\phi}_i}^2$ where the number of each ${\phi_i}$ terms is given by $D_{i,i}$.
+The expression $\sum_{i,j} {\phi}_i\,{\phi}_j D_{i,j}$ rewrites to terms ${ {\phi}_i}^2$ where the number of each ${\phi_i}$ terms is given by $D_{i,i}$.
For each pair of adjacent regions $\{i,j\}$ and $\{j,i\}$, one ${\phi}^2$ term each is contributed, so we can rewrite this in terms of $i \sim j$.
Since $W$ is the adjacency matrix where $w_{ii} = 0, w_{ij} = 1$ if $i$ is a neighbor of $j$, and $w_{ij}=0$ otherwise,
the expression $\sum_{i,j} {\phi}_i\,{\phi}_j W_{i,j}$ rewrite to terms
diff --git a/knitr/movement-hmm/hmm.Rmd b/knitr/movement-hmm/hmm.Rmd
index b21cb42be..d8a727410 100644
--- a/knitr/movement-hmm/hmm.Rmd
+++ b/knitr/movement-hmm/hmm.Rmd
@@ -425,10 +425,10 @@ observing finite subsequences of this unbounded sequence.
\node[state] (C) [right of=B] {$z_3$};
\node[circle] (D) [right of=C] {$\cdots$};
\node[state] (E) [right of=D] {$z_N$};
-\path (A) edge [left] node {{}} (B);
-\path (B) edge [left] node {{}} (C);
-\path (C) edge [left] node {{}} (D);
-\path (D) edge [left] node {{}} (E);
+\path (A) edge [left] node { {} } (B);
+\path (B) edge [left] node { {} } (C);
+\path (C) edge [left] node { {} } (D);
+\path (D) edge [left] node { {} } (E);
\end{tikzpicture}
```