diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..67edb52b --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,31 @@ +cff-version: 1.2.0 +message: "If you use nSTAT, please cite the toolbox paper below." +title: "nSTAT-python: Neural Spike Train Analysis Toolbox for Python" +type: software +license: GPL-2.0-only +repository-code: "https://github.com/cajigaslab/nSTAT-python" +authors: + - family-names: Cajigas + given-names: Iahn + email: icajigas@upenn.edu + - family-names: Malik + given-names: Wasim Q. + - family-names: Brown + given-names: Emery N. +preferred-citation: + type: article + title: "nSTAT: Open-source neural spike train analysis toolbox for Matlab" + authors: + - family-names: Cajigas + given-names: I. + - family-names: Malik + given-names: W. Q. + - family-names: Brown + given-names: E. N. + journal: "Journal of Neuroscience Methods" + year: 2012 + volume: 211 + issue: 2 + start: 245 + end: 264 + doi: "10.1016/j.jneumeth.2012.08.009" diff --git a/README.md b/README.md index 7fca3190..d7a83e29 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # nSTAT-python +**Neural Spike Train Analysis Toolbox for Python** + +[![test-and-build](https://github.com/cajigaslab/nSTAT-python/actions/workflows/ci.yml/badge.svg)](https://github.com/cajigaslab/nSTAT-python/actions/workflows/ci.yml) + `nSTAT-python` is a Python port of the [nSTAT](https://github.com/cajigaslab/nSTAT) open-source neural spike train analysis toolbox. It implements a range of models and algorithms for neural spike train data analysis, with a focus on point-process @@ -11,10 +15,7 @@ continuous neural signals such as LFP, EEG, and ECoG. One of nSTAT's key strengths is point-process generalized linear models for spike train signals that provide a formal statistical framework for processing signals recorded from ensembles of single neurons. It also has extensive support for model -fitting, model-order analysis, and adaptive decoding — including state-space GLM -(SSGLM) estimation via EM, unscented Kalman filtering (UKF), goal-directed -point-process adaptive filters (PPAF), and hybrid discrete/continuous -point-process filters (PPHF). +fitting, model-order analysis, and adaptive decoding. Although created with neural signal processing in mind, nSTAT can be used as a generic tool for analyzing any types of discrete and continuous signals, and thus @@ -25,13 +26,13 @@ suggestions and contributions. This platform is intended as a repository for extensions to the toolbox based on your code contributions as well as for flagging and tracking open issues. -[![test-and-build](https://github.com/cajigaslab/nSTAT-python/actions/workflows/ci.yml/badge.svg)](https://github.com/cajigaslab/nSTAT-python/actions/workflows/ci.yml) +The current release can be installed from PyPI: `pip install nstat-toolbox` Lab websites: - Neuroscience Statistics Research Laboratory: https://www.neurostat.mit.edu - RESToRe Lab: https://www.med.upenn.edu/cajigaslab/ -## Installation +## How to install nSTAT ```bash python -m pip install nstat-toolbox @@ -45,11 +46,7 @@ cd nSTAT-python python -m pip install -e .[dev] ``` -## Example data - -`nSTAT-python` does not commit raw example data to the repository. - -Install the example dataset with: +Install the example dataset: ```bash nstat-install --download-example-data always @@ -59,57 +56,19 @@ Equivalent Python API: ```python from nstat.data_manager import ensure_example_data - data_dir = ensure_example_data(download=True) -print(data_dir) -``` - -## How to install nSTAT (post-install setup) - -Run the setup helper: - -```bash -nstat-install -``` - -Module form: - -```bash -python -m nstat.install --download-example-data never --no-rebuild-doc-search -``` - -Equivalent Python API: - -```python -from nstat.install import nstat_install - -report = nstat_install() -``` - -`clean_user_path_prefs` is accepted for MATLAB-API compatibility, but it is a -Python no-op because import paths are managed by the environment rather than a -MATLAB-style saved user path. - -## Quickstart - -```bash -git clone https://github.com/cajigaslab/nSTAT-python -cd nSTAT-python -python -m pip install -e .[dev] -python -m nstat.install --download-example-data prompt -pytest -q ``` -For a gallery refresh that mirrors the MATLAB repo's paper-example workflow: +Quickstart: ```bash -python tools/paper_examples/build_gallery.py -python tools/parity/build_report.py +cd /path/to/nSTAT-python +pip install -e .[dev] +nstat-install --download-example-data always +pytest -q && python tools/paper_examples/build_gallery.py ``` -## Examples - -### Paper Examples (Self-Contained) +## Paper Examples (Self-Contained) Canonical source files: - `examples/paper/*.py` @@ -136,32 +95,7 @@ refreshes the canonical README paper-example table from Expanded paper-example index and figure gallery: - [docs/paper_examples.md](docs/paper_examples.md) -### Supplementary Examples - -These smaller demos remain useful as quick install and plotting checks. - -| Example | Run command | Output | -|---|---|---| -| Multitaper spectrum + spectrogram | `python examples/readme_examples/example1_multitaper_and_spectrogram.py` | [PNG](examples/readme_examples/images/readme_example1_multitaper_and_spectrogram.png) | -| Simulated CIF spike train | `python examples/readme_examples/example2_simulate_cif_spiketrain_10s.py` | [PNG](examples/readme_examples/images/readme_example2_simulate_cif_spiketrain_10s.png) | -| Spike-train raster | `python examples/readme_examples/example3_nstcoll_raster_from_example2.py` | [PNG](examples/readme_examples/images/readme_example3_nstcoll_raster.png) | -## Documentation - -- Docs home: [cajigaslab.github.io/nSTAT-python](https://cajigaslab.github.io/nSTAT-python/) -- Help home: [cajigaslab.github.io/nSTAT-python/help](https://cajigaslab.github.io/nSTAT-python/help/index.html) -- Paper overview: [cajigaslab.github.io/nSTAT-python/help/paper_overview.html](https://cajigaslab.github.io/nSTAT-python/help/paper_overview.html) -- Example index: [cajigaslab.github.io/nSTAT-python/help/examples_index.html](https://cajigaslab.github.io/nSTAT-python/help/examples_index.html) -- Class definitions: [cajigaslab.github.io/nSTAT-python/help/class_definitions.html](https://cajigaslab.github.io/nSTAT-python/help/class_definitions.html) -- Documentation setup: [cajigaslab.github.io/nSTAT-python/help/examples/DocumentationSetup2025b.html](https://cajigaslab.github.io/nSTAT-python/help/examples/DocumentationSetup2025b.html) - -Source pages: -- [docs/NeuralSpikeAnalysis_top.md](docs/NeuralSpikeAnalysis_top.md) -- [docs/PaperOverview.md](docs/PaperOverview.md) -- [docs/Examples.md](docs/Examples.md) -- [docs/ClassDefinitions.md](docs/ClassDefinitions.md) -- [docs/DocumentationSetup.md](docs/DocumentationSetup.md) - -## Plot Style +Plot style policy: ```python from nstat.plot_style import set_plot_style @@ -173,6 +107,16 @@ set_plot_style('modern') set_plot_style('legacy') ``` +Rendered help documentation (GitHub Pages): +- https://cajigaslab.github.io/nSTAT-python/ + +For mathematical and programmatic details of the toolbox, see: + +Cajigas I, Malik WQ, Brown EN. nSTAT: Open-source neural spike train analysis +toolbox for Matlab. Journal of Neuroscience Methods 211: 245–264, Nov. 2012. +https://doi.org/10.1016/j.jneumeth.2012.08.009 +PMID: 22981419 + ## Paper-Aligned Toolbox Map To keep terminology and workflows consistent with the 2012 toolbox paper, @@ -185,32 +129,17 @@ This page ties the Python toolbox to the paper's workflow categories: `Analysis`, `FitResult`, `DecodingAlgorithms`) - Fitting and assessment workflow (GLM fitting, diagnostics, summaries) - Simulation workflow (conditional intensity and thinning examples) -- State-space GLM (SSGLM) workflow — full EM algorithm (`PPSS_EMFB`) - for across-trial coefficient dynamics with forward-backward Kalman - smoothing (Section 2.4) -- Decoding workflow — point-process adaptive filter (`PPDecodeFilterLinear`), - hybrid filter (`PPHybridFilterLinear`), unscented Kalman filter (`ukf`), - and stimulus confidence intervals (Sections 2.5–2.6) -- Signal processing — multi-taper spectral estimation (`MTMspectrum`), - spectrogram, cross-covariance, and peak-finding methods +- Decoding workflow (univariate/bivariate and history-aware decoding) - Example-to-paper section mapping via `nSTATPaperExamples` -If you use nSTAT in your work, please remember to cite the above paper in any -publications. - -## Developer notes - -- Run tests: - -```bash -pytest -q -``` - -- Build docs: +If you use nSTAT in your work, please remember to cite the above paper in any publications. +nSTAT is protected by the GPL v2 Open Source License. -```bash -sphinx-build -b html docs docs/_build -``` +The code repository for the Python port of nSTAT is hosted on GitHub at +https://github.com/cajigaslab/nSTAT-python. +The paper-example dataset is distributed separately from the Git repository: +- Figshare dataset DOI: https://doi.org/10.6084/m9.figshare.4834640.v3 +- Paper DOI: https://doi.org/10.1016/j.jneumeth.2012.08.009 ## Code audit (2026-03-11) @@ -242,103 +171,6 @@ parity verified. See [parity/report.md](parity/report.md) for the full audit. -## License - -nSTAT is protected by the GPL v2 Open Source License. - -## Cite - -If you use nSTAT in your work, please cite: - -Cajigas I, Malik WQ, Brown EN. nSTAT: Open-source neural spike train analysis -toolbox for Matlab. Journal of Neuroscience Methods 211: 245–264, Nov. 2012. -https://doi.org/10.1016/j.jneumeth.2012.08.009 -PMID: 22981419 - -## Data and Related Repositories - -- **Paper-example dataset (Figshare)**: https://doi.org/10.6084/m9.figshare.4834640.v3 -- **Paper DOI**: https://doi.org/10.1016/j.jneumeth.2012.08.009 - -The code repository for the Python port of nSTAT is hosted on GitHub at -https://github.com/cajigaslab/nSTAT-python. - -## MATLAB / Simulink Dependency for CIF Simulation - -The `CIF` class can simulate spike trains from a fitted conditional intensity -function. The original MATLAB implementation drives this simulation through -**Simulink models** (`PointProcessSimulation.slx`, -`PointProcessSimulationThinning.mdl`, and `SimulatedNetwork2.mdl`). These -models solve the point-process thinning algorithm as a continuous-time -block diagram and produce exact spike-train realisations. - -The Python port includes a **native discrete-time Bernoulli fallback** that -runs without MATLAB, but the results are approximate. For exact parity with -the MATLAB toolbox you need a working MATLAB + Simulink installation and the -MATLAB Engine API for Python. - -### The `backend` flag - -`CIF.simulateCIF()` accepts a `backend` parameter that controls which -simulation engine is used: - -| Flag | Behaviour | -|---|---| -| `backend='auto'` (default) | Uses MATLAB/Simulink when available; silently falls back to the native Python implementation with a `MatlabFallbackWarning` when it is not. | -| `backend='matlab'` | Forces the Simulink backend. Raises `RuntimeError` if MATLAB Engine or the MATLAB nSTAT repo cannot be found. | -| `backend='python'` | Forces the native Python implementation. No MATLAB is required and no warning is issued. | - -### Setting up the MATLAB backend - -1. **Install MATLAB** (R2020a or later recommended) with the **Simulink** - add-on. -2. **Install the MATLAB Engine API for Python** - ([MathWorks instructions](https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html)): - ```bash - cd "$(matlab -batch "disp(matlabroot)" | tail -1)/extern/engines/python" - python -m pip install . - ``` -3. **Point to the MATLAB nSTAT repo** so the engine can find the `.slx` / `.mdl` - models. Use either: - - The `NSTAT_MATLAB_PATH` environment variable: - ```bash - export NSTAT_MATLAB_PATH=/path/to/nSTAT - ``` - - Place the MATLAB repo as a sibling directory named `nSTAT/` next to this - Python repo (auto-detected). - - Or call `set_matlab_nstat_path()` at runtime: - ```python - from nstat.matlab_engine import set_matlab_nstat_path - set_matlab_nstat_path("/path/to/nSTAT") - ``` - -### Why this matters - -The native Python simulation uses a discrete-time Bernoulli draw at each time -step: at every bin the probability of a spike is `p = lambda * delta`, and a -uniform random draw decides whether a spike is emitted. The Simulink model, -by contrast, solves the point-process thinning integral in continuous time, -producing more accurate inter-spike-interval statistics — particularly at high -firing rates or with fast-varying stimuli. - -If your analysis depends on precise spike-timing statistics (e.g. KS -goodness-of-fit tests on simulated data, or decoding benchmarks), use the -MATLAB backend. - -### Call for contributions - -Replacing the Simulink dependency with a pure-Python continuous-time thinning -solver is an open goal for the project. A faithful implementation would need -to: - -- Implement Ogata's modified thinning algorithm (Lewis & Shedler 1979) for - the conditional intensity with history dependence. -- Match the Simulink model's interpolation and adaptive step-size behaviour. -- Pass parity tests against the MATLAB output for the existing paper examples. - -If you are interested in contributing this, please open an issue or pull -request — contributions are very welcome. - ## MATLAB Toolbox The original MATLAB nSTAT toolbox lives in a separate repository: diff --git a/docs/figures/example01/fig01_constant_mg_summary.png b/docs/figures/example01/fig01_constant_mg_summary.png index 97afa7cf..298e5ebf 100644 Binary files a/docs/figures/example01/fig01_constant_mg_summary.png and b/docs/figures/example01/fig01_constant_mg_summary.png differ diff --git a/docs/figures/example01/fig02_washout_raster_overview.png b/docs/figures/example01/fig02_washout_raster_overview.png index 0d8075b5..a3f02e6f 100644 Binary files a/docs/figures/example01/fig02_washout_raster_overview.png and b/docs/figures/example01/fig02_washout_raster_overview.png differ diff --git a/docs/figures/example01/fig03_piecewise_baseline_comparison.png b/docs/figures/example01/fig03_piecewise_baseline_comparison.png index c194ff0d..45397bdb 100644 Binary files a/docs/figures/example01/fig03_piecewise_baseline_comparison.png and b/docs/figures/example01/fig03_piecewise_baseline_comparison.png differ diff --git a/docs/figures/example02/fig01_data_overview.png b/docs/figures/example02/fig01_data_overview.png index 968f5b40..63823417 100644 Binary files a/docs/figures/example02/fig01_data_overview.png and b/docs/figures/example02/fig01_data_overview.png differ diff --git a/docs/figures/example02/fig02_lag_and_model_comparison.png b/docs/figures/example02/fig02_lag_and_model_comparison.png index d0c44b43..748a2175 100644 Binary files a/docs/figures/example02/fig02_lag_and_model_comparison.png and b/docs/figures/example02/fig02_lag_and_model_comparison.png differ diff --git a/docs/figures/example03/fig01_simulated_and_real_rasters.png b/docs/figures/example03/fig01_simulated_and_real_rasters.png index 95ece6e0..4ac7fb64 100644 Binary files a/docs/figures/example03/fig01_simulated_and_real_rasters.png and b/docs/figures/example03/fig01_simulated_and_real_rasters.png differ diff --git a/docs/figures/example03/fig02_psth_comparison.png b/docs/figures/example03/fig02_psth_comparison.png index 3976e35e..68725db6 100644 Binary files a/docs/figures/example03/fig02_psth_comparison.png and b/docs/figures/example03/fig02_psth_comparison.png differ diff --git a/docs/figures/example03/fig03_ssglm_simulation_summary.png b/docs/figures/example03/fig03_ssglm_simulation_summary.png index 7f5ac534..f559d76c 100644 Binary files a/docs/figures/example03/fig03_ssglm_simulation_summary.png and b/docs/figures/example03/fig03_ssglm_simulation_summary.png differ diff --git a/docs/figures/example03/fig04_ssglm_fit_diagnostics.png b/docs/figures/example03/fig04_ssglm_fit_diagnostics.png index de9efc25..9542f35c 100644 Binary files a/docs/figures/example03/fig04_ssglm_fit_diagnostics.png and b/docs/figures/example03/fig04_ssglm_fit_diagnostics.png differ diff --git a/docs/figures/example03/fig05_stimulus_effect_surfaces.png b/docs/figures/example03/fig05_stimulus_effect_surfaces.png index 4e106d80..7c88841e 100644 Binary files a/docs/figures/example03/fig05_stimulus_effect_surfaces.png and b/docs/figures/example03/fig05_stimulus_effect_surfaces.png differ diff --git a/docs/figures/example03/fig06_learning_trial_comparison.png b/docs/figures/example03/fig06_learning_trial_comparison.png index 7bfeccbd..36f32429 100644 Binary files a/docs/figures/example03/fig06_learning_trial_comparison.png and b/docs/figures/example03/fig06_learning_trial_comparison.png differ diff --git a/docs/figures/example04/fig01_example_cells_path_overlay.png b/docs/figures/example04/fig01_example_cells_path_overlay.png index 47f41cdd..89a4491b 100644 Binary files a/docs/figures/example04/fig01_example_cells_path_overlay.png and b/docs/figures/example04/fig01_example_cells_path_overlay.png differ diff --git a/docs/figures/example04/fig02_model_summary_statistics.png b/docs/figures/example04/fig02_model_summary_statistics.png index 5a17f5fe..5d558144 100644 Binary files a/docs/figures/example04/fig02_model_summary_statistics.png and b/docs/figures/example04/fig02_model_summary_statistics.png differ diff --git a/docs/figures/example04/fig03_gaussian_place_fields_animal1.png b/docs/figures/example04/fig03_gaussian_place_fields_animal1.png index b18cb706..57f2cfe2 100644 Binary files a/docs/figures/example04/fig03_gaussian_place_fields_animal1.png and b/docs/figures/example04/fig03_gaussian_place_fields_animal1.png differ diff --git a/docs/figures/example04/fig04_zernike_place_fields_animal1.png b/docs/figures/example04/fig04_zernike_place_fields_animal1.png index c6d47fc7..c51a9996 100644 Binary files a/docs/figures/example04/fig04_zernike_place_fields_animal1.png and b/docs/figures/example04/fig04_zernike_place_fields_animal1.png differ diff --git a/docs/figures/example04/fig05_gaussian_place_fields_animal2.png b/docs/figures/example04/fig05_gaussian_place_fields_animal2.png index b1f79da1..0974d97a 100644 Binary files a/docs/figures/example04/fig05_gaussian_place_fields_animal2.png and b/docs/figures/example04/fig05_gaussian_place_fields_animal2.png differ diff --git a/docs/figures/example04/fig06_zernike_place_fields_animal2.png b/docs/figures/example04/fig06_zernike_place_fields_animal2.png index 35c787ee..4e76d94d 100644 Binary files a/docs/figures/example04/fig06_zernike_place_fields_animal2.png and b/docs/figures/example04/fig06_zernike_place_fields_animal2.png differ diff --git a/docs/figures/example04/fig07_example_cell_mesh_comparison.png b/docs/figures/example04/fig07_example_cell_mesh_comparison.png index a27857fc..b7c5d750 100644 Binary files a/docs/figures/example04/fig07_example_cell_mesh_comparison.png and b/docs/figures/example04/fig07_example_cell_mesh_comparison.png differ diff --git a/docs/figures/example05/fig01_univariate_setup.png b/docs/figures/example05/fig01_univariate_setup.png index b9903f27..b7d538de 100644 Binary files a/docs/figures/example05/fig01_univariate_setup.png and b/docs/figures/example05/fig01_univariate_setup.png differ diff --git a/docs/figures/example05/fig02_univariate_decoding.png b/docs/figures/example05/fig02_univariate_decoding.png index 5ede7289..b8e96a65 100644 Binary files a/docs/figures/example05/fig02_univariate_decoding.png and b/docs/figures/example05/fig02_univariate_decoding.png differ diff --git a/docs/figures/example05/fig03_reach_and_population_setup.png b/docs/figures/example05/fig03_reach_and_population_setup.png index 5d96e220..f10bc678 100644 Binary files a/docs/figures/example05/fig03_reach_and_population_setup.png and b/docs/figures/example05/fig03_reach_and_population_setup.png differ diff --git a/docs/figures/example05/fig04_ppaf_goal_vs_free.png b/docs/figures/example05/fig04_ppaf_goal_vs_free.png index 6d551c7f..dda658cb 100644 Binary files a/docs/figures/example05/fig04_ppaf_goal_vs_free.png and b/docs/figures/example05/fig04_ppaf_goal_vs_free.png differ diff --git a/docs/figures/example05/fig05_hybrid_setup.png b/docs/figures/example05/fig05_hybrid_setup.png index 64304ae9..0c53e806 100644 Binary files a/docs/figures/example05/fig05_hybrid_setup.png and b/docs/figures/example05/fig05_hybrid_setup.png differ diff --git a/docs/figures/example05/fig06_hybrid_decoding_summary.png b/docs/figures/example05/fig06_hybrid_decoding_summary.png index 519f66d4..1b23486c 100644 Binary files a/docs/figures/example05/fig06_hybrid_decoding_summary.png and b/docs/figures/example05/fig06_hybrid_decoding_summary.png differ diff --git a/examples/paper/example01_mepsc_poisson.py b/examples/paper/example01_mepsc_poisson.py index 885cb484..390bf48a 100644 --- a/examples/paper/example01_mepsc_poisson.py +++ b/examples/paper/example01_mepsc_poisson.py @@ -140,34 +140,32 @@ def run_example01(*, export_figures: bool = False, export_dir: Path | None = Non print(f" AIC: {resultConst.AIC}") print(f" BIC: {resultConst.BIC}") - # --- Figure 1: Constant Mg2+ diagnostics (Matlab-matching 2x2 layout) --- - # Matlab uses subplot(2,2,...) with: raster, InvGausTrans, KSPlot, lambda + # --- Figure 1: Constant Mg2+ diagnostics --- + # MATLAB layout: 2x2 with (1,1) raster, (1,2) InvGausTrans, + # (2,1) KSPlot, (2,2) lambda plot. fig1, axes1 = plt.subplots(2, 2, figsize=(14, 9)) - # (2,2,1): Neural raster ax = axes1[0, 0] spikeCollConst.plot(handle=ax) - ax.set_title("Neural Raster with constant Mg$^{2+}$ Concentration", - fontweight="bold", fontsize=12) - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_ylabel("mEPSCs", fontname="Arial", fontsize=12, fontweight="bold") + ax.set_title(r"Neural Raster with constant Mg$^{2+}$ Concentration", + fontweight="bold", fontsize=12, fontfamily="Arial") + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_ylabel("mEPSCs", fontsize=12, fontweight="bold", fontfamily="Arial") ax.set_yticks([0, 1]) - # (2,2,2): Inverse Gaussian transform (ACF) - resultConst.plotInvGausTrans(fit_num=None, handle=axes1[0, 1]) + ax = axes1[0, 1] + resultConst.plotInvGausTrans(handle=ax) - # (2,2,3): KS plot - resultConst.KSPlot(fit_num=None, handle=axes1[1, 0]) + ax = axes1[1, 0] + resultConst.KSPlot(handle=ax) - # (2,2,4): Lambda estimate ax = axes1[1, 1] lam = resultConst.lambda_signal - ax.plot(np.asarray(lam.time, dtype=float), - np.asarray(lam.data[:, 0], dtype=float), - "b", linewidth=2) - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_ylabel(r"$\lambda(t)$ [Hz]", fontname="Arial", fontsize=12, fontweight="bold") - ax.legend(["$\\lambda_{const}$"], loc="upper right", fontsize=14) + ax.plot(lam.time, lam.data[:, 0], "b", linewidth=2) + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_ylabel(f"{lam.name} [{lam.yunits}]" if lam.yunits else lam.name, + fontsize=12, fontweight="bold", fontfamily="Arial") + ax.legend([r"$\lambda_{const}$"], loc="upper right") fig1.tight_layout() figure_files.extend(_maybe_export(fig1, export_dir, "fig01_constant_mg_summary")) @@ -191,16 +189,18 @@ def run_example01(*, export_figures: bool = False, export_dir: Path | None = Non ax = axes2[0] nstConst.plot(handle=ax) ax.set_yticks([0, 1]) - ax.set_ylabel("mEPSCs", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_title("Neural Raster with constant Mg$^{2+}$ Concentration", - fontweight="bold", fontsize=12) + ax.set_ylabel("mEPSCs", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_title(r"Neural Raster with constant Mg$^{2+}$ Concentration", + fontweight="bold", fontsize=12, fontfamily="Arial") + ax.xaxis.label.set(fontsize=12, fontweight="bold", fontfamily="Arial") ax = axes2[1] nstWashout.plot(handle=ax) ax.set_yticks([0, 1]) - ax.set_ylabel("mEPSCs", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_title("Neural Raster with decreasing Mg$^{2+}$ Concentration", - fontweight="bold", fontsize=12) + ax.set_ylabel("mEPSCs", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_title(r"Neural Raster with decreasing Mg$^{2+}$ Concentration", + fontweight="bold", fontsize=12, fontfamily="Arial") + ax.xaxis.label.set(fontsize=12, fontweight="bold", fontfamily="Arial") fig2.tight_layout() figure_files.extend(_maybe_export(fig2, export_dir, "fig02_washout_raster_overview")) @@ -249,38 +249,36 @@ def run_example01(*, export_figures: bool = False, export_dir: Path | None = Non print(f" AIC: {resultWashout.AIC}") print(f" BIC: {resultWashout.BIC}") - # --- Figure 3: Piecewise model diagnostics (Matlab-matching 2x2 layout) --- - # Matlab uses subplot(2,2,...) with: raster+epoch lines, InvGausTrans, KSPlot, lambda comparison + # --- Figure 3: Piecewise model diagnostics --- + # MATLAB layout: 2x2 with (1,1) raster + red epoch lines, + # (1,2) InvGausTrans, (2,1) KSPlot, (2,2) dual-lambda plot. fig3, axes3 = plt.subplots(2, 2, figsize=(14, 9)) - # (2,2,1): Neural raster with epoch boundary lines ax = axes3[0, 0] spikeCollWashout.plot(handle=ax) - ax.set_title("Neural Raster with decreasing Mg$^{2+}$ Concentration", - fontweight="bold", fontsize=12) - ax.set_xlabel("time [s]", fontsize=12, fontweight="bold") + ax.set_title(r"Neural Raster with decreasing Mg$^{2+}$ Concentration", + fontweight="bold", fontsize=12, fontfamily="Arial") + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") ax.set_yticklabels([]) - ax.axvline(495, color="r", linewidth=4) - ax.axvline(765, color="r", linewidth=4) + # Red epoch-transition markers (MATLAB: plot([495;495],[0,1],'r','LineWidth',4)) + ax.plot([495, 495], [0, 1], "r", linewidth=4) + ax.plot([765, 765], [0, 1], "r", linewidth=4) - # (2,2,2): Inverse Gaussian transform (ACF) — all fits - resultWashout.plotInvGausTrans(fit_num=None, handle=axes3[0, 1]) + ax = axes3[0, 1] + resultWashout.plotInvGausTrans(handle=ax) - # (2,2,3): KS plot — all fits - resultWashout.KSPlot(fit_num=None, handle=axes3[1, 0]) + ax = axes3[1, 0] + resultWashout.KSPlot(handle=ax) - # (2,2,4): Lambda comparison (two models overlaid) ax = axes3[1, 1] lam = resultWashout.lambda_signal - t = np.asarray(lam.time, dtype=float) - ax.plot(t, np.asarray(lam.data[:, 0], dtype=float), "b", linewidth=2) - if lam.data.shape[1] > 1: - ax.plot(t, np.asarray(lam.data[:, 1], dtype=float), "g", linewidth=2) + ax.plot(lam.time, lam.getSubSignal(1).data[:, 0], "b", linewidth=2) + ax.plot(lam.time, lam.getSubSignal(2).data[:, 0], "g", linewidth=2) ax.set_ylim(0, 5) - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_ylabel(r"$\lambda(t)$ [Hz]", fontname="Arial", fontsize=12, fontweight="bold") - ax.legend(["$\\lambda_{const}$", "$\\lambda_{const-epoch}$"], - loc="upper right", fontsize=14) + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_ylabel(f"{lam.name} [{lam.yunits}]" if lam.yunits else lam.name, + fontsize=12, fontweight="bold", fontfamily="Arial") + ax.legend([r"$\lambda_{const}$", r"$\lambda_{const-epoch}$"], loc="upper right") fig3.tight_layout() figure_files.extend(_maybe_export(fig3, export_dir, "fig03_piecewise_baseline_comparison")) diff --git a/examples/paper/example02_whisker_stimulus_thalamus.py b/examples/paper/example02_whisker_stimulus_thalamus.py index 99447003..d3a87583 100644 --- a/examples/paper/example02_whisker_stimulus_thalamus.py +++ b/examples/paper/example02_whisker_stimulus_thalamus.py @@ -150,31 +150,43 @@ def run_example02(*, export_figures: bool = False, export_dir: Path | None = Non nstView.setMaxTime(viewWindow) nstView.plot(handle=ax) ax.set_yticks([0, 1]) - ax.set_title("Neural Raster", fontweight="bold", fontsize=16, fontname="Arial") + xticks = np.arange(0, int(max(time)) + 1, 1) + ax.set_xticks(xticks) + ax.set_xticklabels([]) # No x-labels on top/middle subplots ax.set_xlabel("") - ax.set_xticklabels([]) - ax.set_ylabel("spikes", fontname="Arial", fontsize=12, fontweight="bold") + ax.set_ylabel("spikes", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_title("Neural Raster", fontweight="bold", fontsize=16, fontfamily="Arial") + ax.spines["top"].set_linewidth(1) + ax.spines["right"].set_linewidth(1) # Subplot 2: Stimulus displacement (first 21 s, black line matching MATLAB) ax = axes1[1] stimView = stim.getSigInTimeWindow(0, viewWindow) - stimView.plot(handle=ax, plotPropsIn=[["k"]]) - ax.get_legend().remove() if ax.get_legend() else None - ax.set_ylabel("Displacement [mm]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_xlabel("") + ax.plot(stimView.time, stimView.data[:, 0], "k") + ax.legend().set_visible(False) if ax.get_legend() else None + ax.set_yticks(np.arange(0, 1.25, 0.25)) + ax.set_xticks(xticks) ax.set_xticklabels([]) - ax.set_title("Stimulus - Whisker Displacement", fontweight="bold", fontsize=16, fontname="Arial") + ax.set_xlabel("") + ax.set_ylabel("Displacement [mm]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_title("Stimulus - Whisker Displacement", fontweight="bold", fontsize=16, fontfamily="Arial") + for spine in ax.spines.values(): + spine.set_linewidth(1) # Subplot 3: Stimulus velocity (derivative, first 21 s, black line matching MATLAB) ax = axes1[2] stimDeriv = stim.derivative stimDerivView = stimDeriv.getSigInTimeWindow(0, viewWindow) - stimDerivView.plot(handle=ax, plotPropsIn=[["k"]]) - ax.get_legend().remove() if ax.get_legend() else None + ax.plot(stimDerivView.time, stimDerivView.data[:, 0], "k") + ax.set_yticks(np.arange(-80, 81, 40)) + ax.set_xticks(xticks) + ax.set_xlim(0, viewWindow) ax.set_ylim(-80, 80) - ax.set_ylabel("Displacement Velocity [mm/s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_title("Displacement Velocity", fontweight="bold", fontsize=16, fontname="Arial") + ax.set_ylabel("Displacement Velocity [mm/s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_title("Displacement Velocity", fontweight="bold", fontsize=16, fontfamily="Arial") + for spine in ax.spines.values(): + spine.set_linewidth(1) fig1.tight_layout() figure_files.extend(_maybe_export( @@ -332,49 +344,65 @@ def run_example02(*, export_figures: bool = False, export_dir: Path | None = Non # Figure 2: Lag selection, history diagnostics, KS, coefficients # (Matlab uses subplot(7,2,...) layout) # ================================================================== - fig2 = plt.figure(figsize=(14, 12)) + fig2 = plt.figure(figsize=(14, 9)) import matplotlib.gridspec as gridspec gs = gridspec.GridSpec(7, 2, figure=fig2, hspace=0.5, wspace=0.3) + numResults = len(ksArr) + # --- Left column, rows 1-3: Cross-correlation function --- ax_xcov = fig2.add_subplot(gs[0:3, 0]) xcovWindowed.plot(handle=ax_xcov) - ax_xcov.plot(shiftTime, peakVal, "ro", markersize=8, - markerfacecolor="r", markeredgecolor="r", linewidth=3) - ax_xcov.set_title("Residual Cross-Covariance", fontweight="bold") - ax_xcov.set_xlabel("Lag [s]") - ax_xcov.set_ylabel("Cross-covariance") + ax_xcov.plot(shiftTime, peakVal, "ro", linewidth=3, + markerfacecolor="r", markeredgecolor="r") + ax_xcov.set_title( + f"Cross Correlation Function - Peak at t={shiftTime:g} sec", + fontweight="bold", fontsize=12, fontfamily="Arial") + ax_xcov.set_xlabel("Lag [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax_xcov.set_ylabel("") # --- Right column, row 1: KS statistic vs Q --- ax_ks_sweep = fig2.add_subplot(gs[0, 1]) - xvals = np.arange(len(ksArr)) + xvals = np.arange(numResults) ax_ks_sweep.plot(xvals, ksArr, ".-") - if windowIndex < len(ksArr): - ax_ks_sweep.plot(xvals[windowIndex], ksArr[windowIndex], "r*", - markersize=10) - ax_ks_sweep.set_title("KS Statistic vs Q", fontweight="bold") - ax_ks_sweep.set_xlabel("Number of History Windows") - ax_ks_sweep.set_ylabel("KS Stat") + if windowIndex < numResults: + ax_ks_sweep.plot(xvals[windowIndex], ksArr[windowIndex], "r*") + ax_ks_sweep.set_xlim(xvals[0], xvals[-1]) + ax_ks_sweep.set_xticks(np.arange(0, numResults, 5)) + ax_ks_sweep.set_xticklabels([]) + ax_ks_sweep.tick_params(length=4, which="major") + ax_ks_sweep.minorticks_on() + ax_ks_sweep.set_ylabel("KS Statistic") + ax_ks_sweep.set_title("Model Selection via change\nin KS Statistic, AIC, and BIC", + fontweight="bold", fontsize=12, fontfamily="Arial") # --- Right column, row 2: Delta AIC vs Q --- ax_daic = fig2.add_subplot(gs[1, 1]) dAIC_full = aicArr - aicArr[0] ax_daic.plot(np.arange(len(dAIC_full)), dAIC_full, ".-") if windowIndex < len(dAIC_full): - ax_daic.plot(windowIndex, dAIC_full[windowIndex], "r*", markersize=10) - ax_daic.set_title("$\\Delta$AIC vs Q", fontweight="bold") - ax_daic.set_xlabel("Number of History Windows") - ax_daic.set_ylabel("$\\Delta$AIC") + ax_daic.plot(windowIndex, dAIC_full[windowIndex], "r*") + ax_daic.set_xlim(0, numResults - 1) + ax_daic.set_xticks(np.arange(0, numResults, 5)) + ax_daic.set_xticklabels([]) + ax_daic.tick_params(length=4, which="major") + ax_daic.minorticks_on() + ax_daic.set_ylabel(r"$\Delta$ AIC") # --- Right column, row 3: Delta BIC vs Q --- ax_dbic = fig2.add_subplot(gs[2, 1]) dBIC_full = bicArr - bicArr[0] ax_dbic.plot(np.arange(len(dBIC_full)), dBIC_full, ".-") if windowIndex < len(dBIC_full): - ax_dbic.plot(windowIndex, dBIC_full[windowIndex], "r*", markersize=10) - ax_dbic.set_title("$\\Delta$BIC vs Q", fontweight="bold") - ax_dbic.set_xlabel("Number of History Windows") - ax_dbic.set_ylabel("$\\Delta$BIC") + ax_dbic.plot(windowIndex, dBIC_full[windowIndex], "r*") + ax_dbic.set_xlim(0, numResults - 1) + ax_dbic.set_xticks(np.arange(0, numResults, 5)) + ax_dbic.tick_params(length=4, which="major") + ax_dbic.minorticks_on() + ax_dbic.set_xlabel("# History Windows, Q", + fontsize=12, fontweight="bold", fontfamily="Arial") + ax_dbic.set_ylabel(r"$\Delta$ BIC", + fontsize=12, fontweight="bold", fontfamily="Arial") # --- Left column, rows 5-7: KS plot (3 models) --- ax_ks = fig2.add_subplot(gs[4:7, 0]) @@ -383,7 +411,8 @@ def run_example02(*, export_figures: bool = False, export_dir: Path | None = Non # --- Right column, rows 5-7: Coefficient comparison --- ax_coeff = fig2.add_subplot(gs[4:7, 1]) modelCompare.plotCoeffs(handle=ax_coeff) - + if ax_coeff.get_legend(): + ax_coeff.get_legend().set_visible(False) figure_files.extend(_maybe_export( fig2, export_dir, "fig02_lag_and_model_comparison")) diff --git a/examples/paper/example03_psth_and_ssglm.py b/examples/paper/example03_psth_and_ssglm.py index 2dea60f5..a9888b72 100644 --- a/examples/paper/example03_psth_and_ssglm.py +++ b/examples/paper/example03_psth_and_ssglm.py @@ -200,19 +200,19 @@ def run_part_a(data_dir, export_dir=None): ax = axes1[0, 0] ax.plot(time, lambdaData, "b", linewidth=2) ax.set_title("Simulated Conditional Intensity Function (CIF)", - fontweight="bold", fontsize=14, fontname="Arial") - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_ylabel(r"$\lambda(t)$ [spikes/sec]", fontname="Arial", fontsize=12, fontweight="bold") - ax.legend([r"$\lambda_1$"], loc="upper right", fontsize=14) + fontweight="bold", fontsize=14, fontfamily="Arial") + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_ylabel(r"$\lambda(t)$ [spikes/sec]", fontsize=14, fontweight="bold", + fontfamily="Arial") # Bottom-left: simulated raster ax = axes1[1, 0] spikeCollSim.plot(handle=ax) ax.set_yticks(range(0, numRealizations + 1, 5)) ax.set_title(f"{numRealizations} Simulated Point Process Sample Paths", - fontweight="bold", fontsize=14, fontname="Arial") - ax.set_xlabel("time [s]", fontname="Arial", fontsize=12, fontweight="bold") - ax.set_ylabel("Trial [k]", fontname="Arial", fontsize=12, fontweight="bold") + fontweight="bold", fontsize=14, fontfamily="Arial") + ax.set_xlabel("time [s]", fontsize=12, fontweight="bold", fontfamily="Arial") + ax.set_ylabel("Trial [k]", fontsize=12, fontweight="bold", fontfamily="Arial") # Top-right: real cell 6 raster ax = axes1[0, 1] @@ -272,12 +272,13 @@ def run_part_a(data_dir, export_dir=None): # Bottom row: PSTH comparisons ax = axes2[1, 0] h_true, = ax.plot(time, lambdaData, "b", linewidth=4, label="true") - psth_time = np.asarray(psthSim.time, dtype=float).ravel() - psth_data = np.asarray(psthSim.data, dtype=float).ravel() - h_psth, = ax.plot(psth_time, psth_data, "rx", linewidth=4, label="PSTH") + # MATLAB z-order: true, then GLM, then PSTH (markers on top) glm_time = np.asarray(psthGLMSim.time, dtype=float).ravel() glm_data = np.asarray(psthGLMSim.data, dtype=float).ravel() h_glm, = ax.plot(glm_time, glm_data, "k", linewidth=4, label="PSTH_{glm}") + psth_time = np.asarray(psthSim.time, dtype=float).ravel() + psth_data = np.asarray(psthSim.data, dtype=float).ravel() + h_psth, = ax.plot(psth_time, psth_data, "rx", linewidth=4, label="PSTH") ax.legend(handles=[h_true, h_psth, h_glm]) ax.set_xlabel("time [s]") ax.set_ylabel("[spikes/sec]") @@ -405,7 +406,8 @@ def run_part_b(data_dir, export_dir=None): axes3[2, 0].get_position().height] ) ax.imshow(stimData.T, aspect="auto", origin="lower", - extent=[time[0], time[-1], 1, numRealizations]) + extent=[time[0], time[-1], 1, numRealizations], + cmap="jet") # MATLAB default colormap for imagesc ax.set_xlabel("time [s]") ax.set_ylabel("Trial [k]") ax.set_title("True Conditional Intensity Function", fontweight="bold", @@ -501,28 +503,31 @@ def run_part_b(data_dir, export_dir=None): # MATLAB orientation: time [s] on x-axis, Trial [k] on y-axis # (matches fig03 bottom-panel "True Conditional Intensity Function") # ------------------------------------------------------------------ - fig5, axes5 = plt.subplots(3, 1, figsize=(14, 9)) + from mpl_toolkits.mplot3d import Axes3D # noqa: F401 + + fig5 = plt.figure(figsize=(14, 9)) trial_axis = np.arange(1, numRealizations + 1) T_act = min(actStimEffect.shape[0], len(basis_time)) + # Build meshgrid: MATLAB mesh(trial, time, data) — trial on X, time on Y + K_mesh, T_mesh = np.meshgrid(trial_axis, basis_time[:T_act]) + + # MATLAB uses mesh() which renders wireframe only (no filled faces). + # plot_wireframe is the closest matplotlib equivalent. surfaces = [ - (actStimEffect[:T_act, :], "True Stimulus Effect"), - (psthSurface2D[:T_act, :], "PSTH Estimated Stimulus Effect"), - (estStimEffect[:T_act, :], "SSGLM Estimated Stimulus Effect"), + ("True Stimulus Effect", actStimEffect[:T_act, :]), + ("PSTH Estimated Stimulus Effect", psthSurface2D[:T_act, :]), + ("SSGLM Estimated Stimulus Effect", estStimEffect[:T_act, :]), ] - for ax, (data, title_str) in zip(axes5, surfaces): - # data is (T, K) — time on rows, trials on columns - # Transpose so trials are on y-axis and time on x-axis, - # matching MATLAB nSTATPaperExamples_15.png orientation. - ax.pcolormesh(basis_time[:T_act], trial_axis, data.T, cmap="viridis", - shading="auto") - ax.set_xlabel("time [s]") - ax.set_ylabel("Trial [k]") - ax.set_title(title_str, fontweight="bold", fontsize=14) - # Remove redundant per-subplot x-labels except the bottom one - for ax in axes5[:-1]: - ax.set_xlabel("") - ax.set_xticklabels([]) + for idx, (title, data) in enumerate(surfaces, 1): + ax = fig5.add_subplot(3, 1, idx, projection="3d") + ax.plot_wireframe(K_mesh, T_mesh, data, + rstride=5, cstride=1, + linewidth=0.3, color="k") + ax.view_init(elev=-90, azim=90) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_title(title, fontweight="bold", fontsize=14, fontfamily="Arial") fig5.tight_layout() print(" Figure 5: Stimulus effect surfaces (top-down mesh)") @@ -563,9 +568,11 @@ def run_part_b(data_dir, export_dir=None): for k in range(kTrials): for m in range(k + 1, kTrials): if sigMat[k, m] == 1: - ax2.plot(m, k, "r*", markersize=3) + ax2.plot(m, k, "r*", markersize=6) ax2.xaxis.set_ticks_position("top") + ax2.xaxis.set_label_position("top") ax2.yaxis.set_ticks_position("right") + ax2.yaxis.set_label_position("right") ax2.set_xlabel("Trial Number") ax2.set_ylabel("Trial Number") diff --git a/examples/paper/example04_place_cells_continuous_stimulus.py b/examples/paper/example04_place_cells_continuous_stimulus.py index 085c0fd7..421d74e0 100644 --- a/examples/paper/example04_place_cells_continuous_stimulus.py +++ b/examples/paper/example04_place_cells_continuous_stimulus.py @@ -248,21 +248,20 @@ def run_example04(*, export_figures: bool = False, export_dir: Path | None = Non # ================================================================== # Figure 1: Example cells — spike locations over path (2x2) # ================================================================== - exampleCells = [1, 20, 24, 48] # 0-indexed - fig1, axes1 = plt.subplots(2, 2, figsize=(12, 10)) + exampleCells = [1, 20, 24, 48] # 0-indexed (MATLAB: [2 21 25 49]) + fig1, axes1 = plt.subplots(2, 2, figsize=(14, 9)) # MATLAB: 1400x900 for i, cidx in enumerate(exampleCells): ax = axes1.flat[i] - h1, = ax.plot(x1, y1, "b", linewidth=0.5) + h1, = ax.plot(x1, y1, "b-", linewidth=0.5) n = neurons1[min(cidx, nCells1 - 1)] xn = np.asarray(n["xN"].item(), dtype=float).ravel() yn = np.asarray(n["yN"].item(), dtype=float).ravel() h2, = ax.plot(xn, yn, "r.", markersize=7) - ax.set_title(f"Cell#{cidx + 1}", fontweight="bold", fontsize=12, fontname="Arial") - ax.set_xlabel("X Position") - ax.set_ylabel("Y Position") - ax.set_xticks([-1, -0.5, 0, 0.5, 1]) - ax.set_yticks([-1, -0.5, 0, 0.5, 1]) + ax.set_title(f"Cell#{cidx + 1}", fontweight="bold", fontsize=12) + ax.set_xticks(np.arange(-1, 1.5, 0.5)) + ax.set_yticks(np.arange(-1, 1.5, 0.5)) ax.set_aspect("equal") + ax.axis("square") if i == 3: ax.legend([h1, h2], ["Animal Path", "Location at time of spike"]) fig1.tight_layout() @@ -270,25 +269,25 @@ def run_example04(*, export_figures: bool = False, export_dir: Path | None = Non # ================================================================== # Figure 2: Population statistics (1x3 box plots) # ================================================================== - fig2, axes2 = plt.subplots(1, 3, figsize=(14, 5)) + fig2, axes2 = plt.subplots(1, 3, figsize=(14, 9)) # MATLAB: 1400x900 axes2[0].boxplot([dKS1[np.isfinite(dKS1)], dKS2[np.isfinite(dKS2)]], - labels=["Animal 1", "Animal 2"]) - axes2[0].set_ylabel(r"$\Delta$KS (Gaussian - Zernike)") - axes2[0].set_title("KS Statistic Difference") - axes2[0].axhline(0, color="gray", linestyle="--", linewidth=0.5) + tick_labels=["Animal 1", "Animal 2"], + vert=True) + axes2[0].set_title(r"$\Delta$ KS Statistic", fontsize=14, fontweight="bold", + fontfamily="Arial") axes2[1].boxplot([dAIC1[np.isfinite(dAIC1)], dAIC2[np.isfinite(dAIC2)]], - labels=["Animal 1", "Animal 2"]) - axes2[1].set_ylabel(r"$\Delta$AIC (Zernike - Gaussian)") - axes2[1].set_title("AIC Difference") - axes2[1].axhline(0, color="gray", linestyle="--", linewidth=0.5) + tick_labels=["Animal 1", "Animal 2"], + vert=True) + axes2[1].set_title(r"$\Delta$ AIC", fontsize=14, fontweight="bold", + fontfamily="Arial") axes2[2].boxplot([dBIC1[np.isfinite(dBIC1)], dBIC2[np.isfinite(dBIC2)]], - labels=["Animal 1", "Animal 2"]) - axes2[2].set_ylabel(r"$\Delta$BIC (Zernike - Gaussian)") - axes2[2].set_title("BIC Difference") - axes2[2].axhline(0, color="gray", linestyle="--", linewidth=0.5) + tick_labels=["Animal 1", "Animal 2"], + vert=True) + axes2[2].set_title(r"$\Delta$ BIC", fontsize=14, fontweight="bold", + fontfamily="Arial") fig2.tight_layout() @@ -320,8 +319,8 @@ def _plot_heatmaps(fit_results, nCells, title_prefix, design_gauss, nRows = math.ceil(nCells / 7) nCols = 7 - figG, axesG = plt.subplots(nRows, nCols, figsize=(14, 2 * nRows)) - figZ, axesZ = plt.subplots(nRows, nCols, figsize=(14, 2 * nRows)) + figG, axesG = plt.subplots(nRows, nCols, figsize=(14, 9)) + figZ, axesZ = plt.subplots(nRows, nCols, figsize=(14, 9)) if nRows == 1: axesG = axesG[np.newaxis, :] axesZ = axesZ[np.newaxis, :] @@ -343,7 +342,10 @@ def _plot_heatmaps(fit_results, nCells, title_prefix, design_gauss, ax.set_aspect("equal") ax.set_xticks([]) ax.set_yticks([]) - ax.set_title(f"{i + 1}", fontsize=8) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(False) + ax.spines["bottom"].set_visible(False) # Zernike field ax = axesZ[row, col] @@ -355,7 +357,10 @@ def _plot_heatmaps(fit_results, nCells, title_prefix, design_gauss, ax.set_aspect("equal") ax.set_xticks([]) ax.set_yticks([]) - ax.set_title(f"{i + 1}", fontsize=8) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["left"].set_visible(False) + ax.spines["bottom"].set_visible(False) # Hide unused subplots for i in range(nCells, nRows * nCols): @@ -363,7 +368,7 @@ def _plot_heatmaps(fit_results, nCells, title_prefix, design_gauss, axesG[row, col].set_visible(False) axesZ[row, col].set_visible(False) - # Match MATLAB sgtitle format: "Gaussian Place Fields - Animal#N" + # MATLAB: sgtitle('Gaussian Place Fields - Animal#N') animal_num = title_prefix.replace("Animal ", "") figG.suptitle(f"Gaussian Place Fields - Animal#{animal_num}", fontweight="bold", fontsize=12) @@ -397,25 +402,37 @@ def _plot_heatmaps(fit_results, nCells, title_prefix, design_gauss, field_z = _compute_place_field( coeffs_z, gridDesignZern[:, :coeffs_z.size], xx.shape, sr_ex) - fig7 = plt.figure(figsize=(12, 8)) + fig7 = plt.figure(figsize=(14, 9)) ax3d = fig7.add_subplot(111, projection="3d") - ax3d.plot_surface(xx, yy, field_g, alpha=0.3, color="blue", - label="Gaussian") - ax3d.plot_surface(xx, yy, field_z, alpha=0.3, color="green", - label="Zernike") - # Overlay animal path at z=0 - ax3d.plot(x1, y1, np.zeros_like(x1), "b-", linewidth=0.3, alpha=0.3) - # Overlay spike locations + # MATLAB: mesh(xGrid, yGrid, lambda, 'FaceAlpha',0.2, 'EdgeAlpha',0.2, 'EdgeColor','b'/'g') + # plot_wireframe matches MATLAB's mesh() (wireframe only, no filled faces) + ax3d.plot_wireframe(xx, yy, field_g, color="b", alpha=0.2, + rstride=5, cstride=5, linewidth=0.3) + ax3d.plot_wireframe(xx, yy, field_z, color="g", alpha=0.2, + rstride=5, cstride=5, linewidth=0.3) + # Overlay animal path at z=0 (MATLAB: 'k') + ax3d.plot(x1, y1, np.zeros_like(x1), "k-", linewidth=0.3) + # Overlay spike locations (MATLAB: 'r.') n_ex = neurons1[exampleCell] xn_ex = np.asarray(n_ex["xN"].item(), dtype=float).ravel() yn_ex = np.asarray(n_ex["yN"].item(), dtype=float).ravel() - ax3d.scatter(xn_ex, yn_ex, np.zeros_like(xn_ex), c="r", s=5, - alpha=0.5) - ax3d.set_xlabel("x") - ax3d.set_ylabel("y") - ax3d.set_zlabel("Firing Rate") - ax3d.set_title(f"Cell {exampleCell + 1}: Gaussian (blue) vs Zernike (green)", - fontweight="bold", fontsize=14) + ax3d.scatter(xn_ex, yn_ex, np.zeros_like(xn_ex), c="r", s=5) + ax3d.set_xlabel("x position") + ax3d.set_ylabel("y position") + ax3d.set_title(f"Animal#1, Cell#{exampleCell + 1}", + fontweight="bold", fontsize=12) + # MATLAB legend (wireframe lines, not filled patches) + from matplotlib.lines import Line2D + legend_elements = [ + Line2D([0], [0], color="b", alpha=0.5, linewidth=1.5, + label=r"$\lambda_{Gaussian}$"), + Line2D([0], [0], color="g", alpha=0.5, linewidth=1.5, + label=r"$\lambda_{Zernike}$"), + Line2D([0], [0], color="k", linewidth=0.5, label="Animal Path"), + Line2D([0], [0], marker=".", color="r", linestyle="", + label="Spike Locations"), + ] + ax3d.legend(handles=legend_elements, loc="best") print(f" Figure 7: 3D mesh for cell {exampleCell + 1}") diff --git a/examples/paper/example05_decoding_ppaf_pphf.py b/examples/paper/example05_decoding_ppaf_pphf.py index 9b6cfe90..db8813f8 100644 --- a/examples/paper/example05_decoding_ppaf_pphf.py +++ b/examples/paper/example05_decoding_ppaf_pphf.py @@ -13,26 +13,28 @@ 3. Decode the stimulus using ``PPDecodeFilterLinear`` (PPAF). Part B — 4-State Arm Reach with PPAF (Figures 3–4): - 4. Simulate minimum-energy reaching trajectory (position + velocity, 4-D). + 4. Simulate reaching trajectories (position + velocity, 4-D state) using + minimum-jerk dynamics (cosine acceleration toward target). 5. Encode with 20-cell velocity-tuned population (binomial CIF). - 6. Decode with PPAF (free) and PPAF + Goal; overlay 20 simulations. + 6. Decode with PPAF (free) and PPAF + Goal; compare across 20 simulations. Part C — Hybrid Filter (Figures 5–6): - 7. Load hybrid-filter trajectory fixture with 2 discrete movement states. - 8. Encode with 40-cell population (velocity-tuned, binomial CIF). + 7. Load fixture trajectory with 6-D state (pos + vel + accel) and 2 discrete + movement modes (not-moving / moving) from ``paperHybridFilterExample.mat``. + 8. Simulate 40-cell population with velocity-tuned binomial CIF. 9. Decode joint discrete + continuous state via ``PPHybridFilterLinear`` - over 20 simulations; average results. + (both goal-directed and free), averaged over 20 simulations. Paper mapping: Sections 2.3.6–2.3.7 (decoding); Figs. 8, 9, 14 plus hybrid extension. Expected outputs: - - Figure 1: Driving stimulus, CIFs, and simulated spike raster. - - Figure 2: Decoded stimulus vs. true with 95% CIs. - - Figure 3: Reach path, neural raster, position/velocity traces, CIFs. - - Figure 4: 20-simulation overlaid decoded reach paths (PPAF vs PPAF+Goal). - - Figure 5: Hybrid setup — reach path, raster, kinematics, discrete state. - - Figure 6: Hybrid 20-sim averaged decoding — state, reach path, kinematics. + - Figure 1: CIF tuning curves and simulated spike raster. + - Figure 2: Decoded stimulus vs true (with 95% confidence band). + - Figure 3: Reach trajectory, position/velocity traces, neural raster, CIF. + - Figure 4: PPAF decoding overlaid trajectories (free=green, goal=blue). + - Figure 5: Hybrid fixture setup (reach path, traces, raster, discrete state). + - Figure 6: Hybrid decoding summary (state est, probabilities, decoded path). """ from __future__ import annotations @@ -75,6 +77,33 @@ def _simulate_binomial_spikes_from_lambda(lambdaRate, delta, rng): return (rng.random(prob.shape) < prob).astype(float) +def _simulate_binomial_spikes(xState, muCoeffs, beta, rng, delta=0.001): + """Simulate binomial spikes from state, encoding coefficients, and tuning. + + Computes CIF via logistic link from state and beta, then draws spikes. + + Parameters + ---------- + xState : (ns, T) array — kinematic state (position + velocity) + muCoeffs : (C,) array — baseline log-rate per cell + beta : (ns, C) array — tuning coefficients per state dimension per cell + rng : numpy Generator + delta : float — bin width in seconds (default 0.001) + + Returns + ------- + dN : (C, T) array — binary spike indicators + """ + T = xState.shape[1] + C = len(muCoeffs) + # Build design matrix: (T, 1+ns) = [1, x1, x2, ..., xns] + dataMat = np.column_stack([np.ones(T), xState.T]) + # Build coefficient matrix: (C, 1+ns) = [mu, beta_1, ..., beta_ns] + coeffs = np.column_stack([muCoeffs, beta.T]) + lambdaRate = _logistic_cif(dataMat, coeffs, delta) + return _simulate_binomial_spikes_from_lambda(lambdaRate, delta, rng) + + def _logistic_cif(dataMat, coeffs, delta): """Compute binomial CIF rates matching MATLAB's logistic link. @@ -173,14 +202,19 @@ def _run_part_a(seed=0, n_cells=20): # ────────────────────────────────────────────────────────────────────────────── -def _generate_reach_trajectory(delta, T_total, x0, xT): - """Generate minimum-energy reaching trajectory matching MATLAB. +def _simulate_reach_minjerk(delta, T_total): + """Simulate a 2-D minimum-jerk reach from x0 to xT. + + Uses MATLAB's cosine-acceleration dynamics: + xState(:,k) = A * xState(:,k-1) + (delta/2)*(pi/T)^2 * cos(pi*t/T) + * [0; 0; xT(1)-x0(1); xT(2)-x0(2)] - Uses the forcing function: - x(k) = A*x(k-1) + (delta/2)*(pi/T)^2*cos(pi*t/T)*[0; 0; dx; dy] + Returns time, xState (4×T), A (4×4). """ - time = np.arange(0.0, T_total + delta / 2, delta) - nT = len(time) + x0 = np.array([0.0, 0.0, 0.0, 0.0]) + xT_target = np.array([-0.35, 0.2, 0.0, 0.0]) + time = np.arange(0.0, T_total + delta, delta) + T = len(time) A = np.array([ [1, 0, delta, 0], @@ -189,101 +223,98 @@ def _generate_reach_trajectory(delta, T_total, x0, xT): [0, 0, 0, 1], ], dtype=float) - xState = np.zeros((4, nT)) + xState = np.zeros((4, T), dtype=float) xState[:, 0] = x0 - for k in range(1, nT): - forcing = (delta / 2.0) * (np.pi / T_total) ** 2 * np.cos(np.pi * time[k] / T_total) * \ - np.array([0.0, 0.0, xT[0] - x0[0], xT[1] - x0[1]]) - xState[:, k] = A @ xState[:, k - 1] + forcing + accel_dir = np.array([0.0, 0.0, xT_target[0] - x0[0], xT_target[1] - x0[1]]) + for k in range(1, T): + accel = (delta / 2.0) * (np.pi / T_total) ** 2 * np.cos(np.pi * time[k] / T_total) + xState[:, k] = A @ xState[:, k - 1] + accel * accel_dir return time, xState, A def _run_part_b(seed=0, n_cells=20, n_sims=20): - """Arm reaching simulation and PPAF decoding — matches MATLAB exactly. + """Compare PPAF free vs goal-directed decoding for arm reach. - Generates minimum-energy reach, encodes with velocity-tuned binomial CIF, - decodes with PPAF (free) and PPAF+Goal over 20 simulations. + Matches MATLAB: single trajectory, 20 re-randomized encoding simulations. """ rng = np.random.default_rng(seed) - delta = 0.001 - T_total = 2.0 - - x0 = np.array([0.0, 0.0, 0.0, 0.0]) - xT_target = np.array([-0.35, 0.2, 0.0, 0.0]) - - time, xState, A = _generate_reach_trajectory(delta, T_total, x0, xT_target) - nT = len(time) - xT_actual = xState[:, -1] + delta = 0.001 # 1 ms bins (matches MATLAB) + T_total = 2.0 # 2-second reach - # Process noise: Qreach = diag(var(diff(xState))) * 100 - Qreach = np.diag(np.var(np.diff(xState, axis=1), axis=1)) * 100 + # ── Generate minimum-jerk reach trajectory ── + time, xState, A = _simulate_reach_minjerk(delta, T_total) + T = xState.shape[1] + ns = 4 - # First simulation: generate CIFs and spike data for Figure 3 - bCoeffs = 10.0 * (rng.random((n_cells, 2)) - 0.5) - muCoeffs = np.log(10.0 * delta) + rng.standard_normal(n_cells) - coeffs = np.column_stack([muCoeffs, bCoeffs]) # (C, 3) - dataMat = np.column_stack([np.ones(nT), xState[2, :], xState[3, :]]) # (T, 3): [1, vx, vy] + # Target = final state + yT = xState[:, -1].copy() - # Compute CIF for all cells - lambdaAll = _logistic_cif(dataMat, coeffs, delta) + # Q from trajectory variance (MATLAB: diag(var(diff(xState,[],2),[],2))*100) + Q = np.diag(np.var(np.diff(xState, axis=1), axis=1)) * 100.0 - # Simulate spikes - dN_setup = _simulate_binomial_spikes_from_lambda(lambdaAll, delta, rng) + # Initial/target covariances (MATLAB: very tight) + r, p = 1e-6, 1e-6 + pi0 = np.diag([r, r, p, p]) + piT = np.diag([r, r, p, p]) - # Store setup data for Figure 3 - setup_data = { - "time": time, - "xState": xState, - "lambdaAll": lambdaAll, - "dN": dN_setup, - "n_cells": n_cells, - } - - # 20 repeated simulations for Figure 4 - all_x_u_goal = [] # PPAF+Goal decoded paths - all_x_u_free = [] # PPAF free decoded paths + # ── Run 20 repeated simulations ── + # Same trajectory, re-randomized encoding + spikes each time + all_runs_goal = [] + all_runs_free = [] + example_run = None - for k in range(n_sims): - bCoeffs_k = 10.0 * (rng.random((n_cells, 2)) - 0.5) - muCoeffs_k = np.log(10.0 * delta) + rng.standard_normal(n_cells) - coeffs_k = np.column_stack([muCoeffs_k, bCoeffs_k]) + for sim_idx in range(n_sims): + # MATLAB: bCoeffs = 10*(rand(numCells,2)-0.5); Uniform[-5,5] + bCoeffs = 10.0 * (rng.random((n_cells, 2)) - 0.5) + # MATLAB: muCoeffs = log(10*delta) + randn(numCells,1) + muCoeffs = np.log(10.0 * delta) + rng.standard_normal(n_cells) - lambdaK = _logistic_cif(dataMat, coeffs_k, delta) - dN_k = _simulate_binomial_spikes_from_lambda(lambdaK, delta, rng) - dN_k = np.minimum(dN_k, 1.0) # cap at 1 + # beta: 4×C with zeros for position, bCoeffs for velocity + beta = np.zeros((ns, n_cells), dtype=float) + beta[2, :] = bCoeffs[:, 0] # vx tuning + beta[3, :] = bCoeffs[:, 1] # vy tuning - # beta for decoding: (4, C) — zeros for position, bCoeffs for velocity - beta_k = np.zeros((4, n_cells)) - beta_k[2, :] = bCoeffs_k[:, 0] - beta_k[3, :] = bCoeffs_k[:, 1] + # Simulate spikes + dN = _simulate_binomial_spikes(xState, muCoeffs, beta, rng) - Pi0 = np.diag([1e-6, 1e-6, 1e-6, 1e-6]) - PiT = np.diag([1e-6, 1e-6, 1e-6, 1e-6]) + # Initial state + x0 = np.array([0.0, 0.0, 0.0, 0.0]) - # PPAF+Goal + # --- Goal-directed decode --- _, _, x_u_goal, _, _, _, _, _ = DecodingAlgorithms.PPDecodeFilterLinear( - A, Qreach, dN_k, muCoeffs_k, beta_k, "binomial", delta, - None, None, x0, Pi0, xT_actual, PiT, 0 + A, Q, dN, muCoeffs, beta, "binomial", delta, + None, None, x0, pi0, yT, piT, 0 ) - # PPAF free + # --- Free decode (no goal) --- _, _, x_u_free, _, _, _, _, _ = DecodingAlgorithms.PPDecodeFilterLinear( - A, Qreach, dN_k, muCoeffs_k, beta_k, "binomial", delta, - None, None, x0, Pi0 + A, Q, dN, muCoeffs, beta, "binomial", delta, + None, None, x0, ) - all_x_u_goal.append(x_u_goal) - all_x_u_free.append(x_u_free) + all_runs_goal.append(x_u_goal) + all_runs_free.append(x_u_free) + + if sim_idx == 0: + example_run = { + "time": time, + "xState": xState, + "dN": dN, + "muCoeffs": muCoeffs, + "bCoeffs": bCoeffs, + "beta": beta, + } return { - "setup": setup_data, - "time": time, - "xState": xState, - "all_x_u_goal": all_x_u_goal, - "all_x_u_free": all_x_u_free, + "all_runs_goal": all_runs_goal, + "all_runs_free": all_runs_free, + "example": example_run, "n_cells": n_cells, "n_sims": n_sims, + "xState": xState, + "time": time, + "delta": delta, } @@ -293,181 +324,179 @@ def _run_part_b(seed=0, n_cells=20, n_sims=20): def _load_hybrid_fixture(): - """Load the hybrid filter trajectory fixture (HDF5 preferred, .mat fallback).""" - # Prefer HDF5 (needs h5py; fall back to .mat via scipy if unavailable) - h5_path = REPO_ROOT / "data_cache" / "nstat_data" / "paperHybridFilterExample.h5" - try: - import h5py # noqa: F811 - except ImportError: - h5py = None # type: ignore[assignment] - if h5py is not None and h5_path.exists(): - with h5py.File(str(h5_path), "r") as f: - d = { - "time": f["time"][:], - "delta": float(f["delta"][()]), - "X": f["X"][:], - "mstate": f["mstate"][:].astype(int), - "p_ij": f["p_ij"][:], - "A": [f[f"A/{i}"][:] for i in range(2)], - "Q": [f[f"Q/{i}"][:] for i in range(2)], - "Px0": [f[f"Px0/{i}"][:] for i in range(2)], - "ind": [f[f"ind/{i}"][:].astype(int) for i in range(2)], - } - return d + """Load the MATLAB hybrid filter fixture (paperHybridFilterExample.mat). - # Fallback: .mat file (scipy required) + Returns a dict with: time, delta, X (6×T), mstate (T,), + A (list of 2), Q (list of 2), p_ij (2×2), Px0 (list of 2), ind. + """ import scipy.io as sio + + # Search for fixture in multiple locations candidates = [ - REPO_ROOT / "data_cache" / "nstat_data" / "paperHybridFilterExample.mat", - REPO_ROOT.parent / "nSTAT_currentRelease_Local" / "helpfiles" / "paperHybridFilterExample.mat", + REPO_ROOT / "nstat" / "data" / "paperHybridFilterExample.mat", + REPO_ROOT / "helpfiles" / "paperHybridFilterExample.mat", ] - for path in candidates: - if path.exists(): - d = sio.loadmat(str(path), squeeze_me=True) - # Normalize cell arrays to lists - d["A"] = [d["A"][i] for i in range(2)] - d["Q"] = [d["Q"][i] for i in range(2)] - d["Px0"] = [d["Px0"][i] for i in range(2)] - d["ind"] = [d["ind"][i].flatten().astype(int) for i in range(2)] - return d - raise FileNotFoundError( - "Cannot find paperHybridFilterExample.h5 or .mat. " - "Run the MATLAB export script or copy the data to data_cache/nstat_data/." - ) + mat_path = None + for p in candidates: + if p.exists() and p.stat().st_size > 200: # skip LFS pointers + mat_path = p + break + + if mat_path is None: + raise FileNotFoundError( + "Cannot find paperHybridFilterExample.mat fixture. " + "Ensure it is in nstat/data/ or helpfiles/." + ) + + f = sio.loadmat(str(mat_path)) + time = f["time"].ravel().astype(float) + delta = float(f["delta"].ravel()[0]) + X = f["X"].astype(float) # (6, T) + mstate = f["mstate"].ravel().astype(int) # (T,), values 1 or 2 + p_ij = f["p_ij"].astype(float) # (2, 2) + + # Cell arrays → Python lists + A_cell = f["A"] + Q_cell = f["Q"] + Px0_cell = f["Px0"] + ind_cell = f["ind"] + + A = [A_cell[0, i].astype(float) for i in range(A_cell.shape[1])] + Q = [Q_cell[0, i].astype(float) for i in range(Q_cell.shape[1])] + Px0 = [Px0_cell[0, i].astype(float) for i in range(Px0_cell.shape[1])] + # ind: convert from MATLAB 1-indexed to Python 0-indexed + ind = [ind_cell[0, i].ravel().astype(int) - 1 for i in range(ind_cell.shape[1])] + + return { + "time": time, + "delta": delta, + "X": X, + "mstate": mstate, + "A": A, + "Q": Q, + "p_ij": p_ij, + "Px0": Px0, + "ind": ind, + } def _run_part_c(seed=0, n_cells=40, n_sims=20): """PPHybridFilterLinear: joint discrete/continuous state decoding. - Loads trajectory fixture, encodes with velocity-tuned binomial CIF, - runs 20 simulations and averages decoded results — matching MATLAB. + Loads fixture trajectory, runs 20 simulations with re-randomized encoding, + comparing goal-directed vs free hybrid decoding. """ rng = np.random.default_rng(seed) - fixture = _load_hybrid_fixture() - time = fixture["time"] - delta = float(fixture["delta"]) - X = fixture["X"] # (6, T) - mstate = fixture["mstate"].astype(int) - A_list = list(fixture["A"]) - Q_list = list(fixture["Q"]) - p_ij = fixture["p_ij"] - ind = [np.asarray(v).flatten().astype(int) - 1 for v in fixture["ind"]] # 0-based - Px0 = list(fixture["Px0"]) - - nT = len(time) - - # Clamp hold-state noise (matches MATLAB: minCovVal = 1e-12) - Q_list[0] = 1e-12 * np.eye(Q_list[0].shape[0]) - - # Compute actual process noise from trajectory - nonMovingInd = np.intersect1d(np.where(X[4, :] == 0)[0], np.where(X[5, :] == 0)[0]) - movingInd = np.setdiff1d(np.arange(nT), nonMovingInd) - if len(movingInd) > 1: - Q_list[1] = np.diag(np.var(np.diff(X[:, movingInd], axis=1), axis=1)) - Q_list[1][:4, :4] = 0.0 - if len(nonMovingInd) > 1: - varNV = np.diag(np.var(np.diff(X[:, nonMovingInd], axis=1), axis=1)) - n0 = Q_list[0].shape[0] - Q_list[0] = varNV[:n0, :n0] - - # Setup encoding: first simulation for Figure 5 - muCoeffs_0 = np.log(10.0 * delta) + rng.standard_normal(n_cells) - coeffs_0 = np.column_stack([ - muCoeffs_0, - np.zeros((n_cells, 2)), - 10.0 * (rng.random((n_cells, 2)) - 0.5), - np.zeros((n_cells, 2)), - ]) # (C, 7): [mu, 0, 0, b_vx, b_vy, 0, 0] - - dataMat = np.column_stack([np.ones(nT), X.T]) # (T, 7) - lambdaAll_0 = _logistic_cif(dataMat, coeffs_0, delta) - dN_0 = _simulate_binomial_spikes_from_lambda(lambdaAll_0, delta, rng) - - # Setup data for Figure 5 - setup_data = { - "time": time, - "X": X, - "mstate": mstate, - "dN": dN_0, - "n_cells": n_cells, - } - - # 20 repeated simulations for Figure 6 - X_estAll = np.zeros((X.shape[0], nT, n_sims)) - X_estNTAll = np.zeros((X.shape[0], nT, n_sims)) - S_estAll = np.zeros((n_sims, nT)) - S_estNTAll = np.zeros((n_sims, nT)) - MU_estAll = [] - MU_estNTAll = [] + # ── Load fixture ── + fix = _load_hybrid_fixture() + time = fix["time"] + delta = fix["delta"] + X = fix["X"] # (6, T) + mstate = fix["mstate"] # (T,) + p_ij = fix["p_ij"] + A_models = fix["A"] # [A_hold (2×2), A_reach (6×6)] + Q_models_orig = fix["Q"] # [Q_hold (2×2), Q_reach (6×6)] + Px0 = fix["Px0"] # [Px0_hold (2×2), Px0_reach (6×6)] + ind = fix["ind"] # [[0,1], [0,1,2,3,4,5]] + T = X.shape[1] + + # ── Recompute Q from trajectory variance (matching MATLAB) ── + nonMovingInd = np.where((X[4, :] == 0) & (X[5, :] == 0))[0] + movingInd = np.setdiff1d(np.arange(T), nonMovingInd) + + Q_reach = np.diag(np.var(np.diff(X[:, movingInd], axis=1), axis=1)) + Q_reach[:4, :4] = 0.0 # Zero out pos/vel noise; only accel has noise + varNV = np.diag(np.var(np.diff(X[:, nonMovingInd], axis=1), axis=1)) + Q_hold = varNV[:2, :2] + + Q_models = [Q_hold, Q_reach] + + # State dimensions + dim_hold = A_models[0].shape[0] # 2 + dim_reach = A_models[1].shape[0] # 6 + + # ── Run 20 repeated simulations ── + X_estAll = np.zeros((dim_reach, T, n_sims), dtype=float) + X_estNTAll = np.zeros((dim_reach, T, n_sims), dtype=float) + S_estAll = np.zeros((n_sims, T), dtype=int) + S_estNTAll = np.zeros((n_sims, T), dtype=int) + MU_estAll = np.zeros((2, T, n_sims), dtype=float) + MU_estNTAll = np.zeros((2, T, n_sims), dtype=float) + example_dN = None for n in range(n_sims): - muCoeffs_n = np.log(10.0 * delta) + rng.standard_normal(n_cells) - coeffs_n = np.column_stack([ - muCoeffs_n, - np.zeros((n_cells, 2)), - 10.0 * (rng.random((n_cells, 2)) - 0.5), - np.zeros((n_cells, 2)), - ]) - - lambdaAll_n = _logistic_cif(dataMat, coeffs_n, delta) - dN_n = _simulate_binomial_spikes_from_lambda(lambdaAll_n, delta, rng) - dN_n = np.minimum(dN_n, 1.0) - - mu0 = 0.5 * np.ones(p_ij.shape[0]) - beta_full = coeffs_n[:, 1:].T # (6, C) - # Per-model beta slices: model 0 uses ind[0] states, model 1 uses ind[1] - beta_list = [beta_full[ind[0], :], beta_full[ind[1], :]] + # MATLAB: muCoeffs = log(10*delta) + randn(numCells,1) + muCoeffs = np.log(10.0 * delta) + rng.standard_normal(n_cells) + # MATLAB: coeffs = [muCoeffs, zeros(C,2), 10*(rand(C,2)-0.5), zeros(C,2)] + # = [mu, 0, 0, b_vx, b_vy, 0, 0] — tuned to velocities (states 3-4) + bCoeffs_vx = 10.0 * (rng.random(n_cells) - 0.5) + bCoeffs_vy = 10.0 * (rng.random(n_cells) - 0.5) + + # Full beta: 6×C matrix + beta_full = np.zeros((6, n_cells), dtype=float) + beta_full[2, :] = bCoeffs_vx # vx tuning + beta_full[3, :] = bCoeffs_vy # vy tuning + # Simulate spikes from full state trajectory + dN = _simulate_binomial_spikes(X, muCoeffs, beta_full, rng) + + if n == 0: + example_dN = dN.copy() + + # ── Initial conditions per mode ── x0_list = [X[ind[0], 0], X[ind[1], 0]] + Pi0_list = Px0 + + # ── Target conditions per mode ── yT_list = [X[ind[0], -1], X[ind[1], -1]] - PiT_list = [ - 1e-9 * np.eye(len(ind[0])), - 1e-9 * np.eye(len(ind[1])), - ] + piT_list = [1e-9 * np.eye(dim_hold), 1e-9 * np.eye(dim_reach)] + + # beta per mode: hold uses 2-dim subset, reach uses full 6-dim + beta_hold = beta_full[ind[0], :] # (2, C) — will be zeros + beta_reach = beta_full[ind[1], :] # (6, C) — has vx/vy tuning - # PPAF+Goal (with target) + mu0 = np.array([0.5, 0.5]) + + # --- Goal-directed hybrid decode --- S_est, X_est, _, MU_est, _, _, _ = DecodingAlgorithms.PPHybridFilterLinear( - A_list, Q_list, p_ij, mu0, dN_n, - muCoeffs_n, beta_list, "binomial", delta, - None, None, x0_list, Px0, yT_list, PiT_list + A_models, Q_models, p_ij, mu0, dN, + [muCoeffs, muCoeffs], + [beta_hold, beta_reach], + "binomial", delta, None, None, + x0_list, Pi0_list, + yT_list, piT_list, ) - # PPAF free (no target) + # --- Free hybrid decode (no target) --- S_estNT, X_estNT, _, MU_estNT, _, _, _ = DecodingAlgorithms.PPHybridFilterLinear( - A_list, Q_list, p_ij, mu0, dN_n, - muCoeffs_n, beta_list, "binomial", delta, - None, None, x0_list, Px0 + A_models, Q_models, p_ij, mu0, dN, + [muCoeffs, muCoeffs], + [beta_hold, beta_reach], + "binomial", delta, None, None, + x0_list, Pi0_list, ) X_estAll[:, :, n] = X_est X_estNTAll[:, :, n] = X_estNT S_estAll[n, :] = S_est S_estNTAll[n, :] = S_estNT - MU_estAll.append(MU_est) - MU_estNTAll.append(MU_estNT) - - MU_estAll = np.array(MU_estAll) # (n_sims, n_modes, T) - MU_estNTAll = np.array(MU_estNTAll) # (n_sims, n_modes, T) + MU_estAll[:, :, n] = MU_est + MU_estNTAll[:, :, n] = MU_estNT - state_acc = float(np.mean(np.mean(S_estAll, axis=0).round() == mstate)) - rmse_x = float(np.sqrt(np.mean((np.mean(X_estAll[0, :, :], axis=1) - X[0, :]) ** 2))) - rmse_y = float(np.sqrt(np.mean((np.mean(X_estAll[1, :, :], axis=1) - X[1, :]) ** 2))) + print(f" Hybrid sim {n + 1}/{n_sims} done") return { - "setup": setup_data, "time": time, "X": X, "mstate": mstate, + "dN": example_dN, "X_estAll": X_estAll, "X_estNTAll": X_estNTAll, "S_estAll": S_estAll, "S_estNTAll": S_estNTAll, "MU_estAll": MU_estAll, "MU_estNTAll": MU_estNTAll, - "state_acc": state_acc, - "rmse_x": rmse_x, - "rmse_y": rmse_y, "n_cells": n_cells, "n_sims": n_sims, } @@ -485,8 +514,8 @@ def _plot_part_a(result): dN = result["dN"] n_cells = result["n_cells"] - # ── Figure 1: 3×1 matching MATLAB subplot(3,1,...) ── - fig1, axes1 = plt.subplots(3, 1, figsize=(10, 8), sharex=True) + # ── Figure 1: stimulus, CIF, spike raster (3 panels, matching MATLAB) ── + fig1, axes1 = plt.subplots(3, 1, figsize=(14, 9), sharex=True) # (3,1,1): Driving stimulus axes1[0].plot(time, stimSignal, "k", linewidth=1.5) @@ -506,9 +535,8 @@ def _plot_part_a(result): # (3,1,3): Spike raster for c in range(n_cells): - spike_t = time[dN[c, :] > 0] - axes1[2].plot(spike_t, np.full_like(spike_t, c + 1), "|", color="k", - markersize=2) + spike_times = time[dN[c, :] > 0] + axes1[2].plot(spike_times, np.full_like(spike_times, c + 1), "|", color="k", markersize=4) axes1[2].set_ylabel("Cell Number") axes1[2].set_xlabel("time [s]") axes1[2].set_ylim(0.5, n_cells + 0.5) @@ -517,63 +545,47 @@ def _plot_part_a(result): fontsize=14, fontfamily="Arial") fig1.tight_layout() - # ── Figure 2: Decoded vs true (single axes, MATLAB style) ── - # MATLAB uses thin CI *lines* (not shaded fill), thick decoded/actual - fig2, ax2 = plt.subplots(1, 1, figsize=(10, 4)) - ax2.plot(time, result["ci_low"], "k-", linewidth=0.5) - ax2.plot(time, result["ci_high"], "k-", linewidth=0.5) - hEst, = ax2.plot(time, result["x_decoded"], "k-", linewidth=4.0) - hAct, = ax2.plot(time, stimSignal, "b-", linewidth=4.0) - ax2.legend([hEst, hAct], ["Decoded", "Actual"], loc="upper right") - ax2.set_xlabel("time [s]") - ax2.set_ylabel("Stimulus") - ax2.set_title( - f"Decoded Stimulus +/- 95% CIs with {n_cells} cells", - fontweight="bold", fontsize=18, fontfamily="Arial", + # ── Figure 2: Decoding results (MATLAB: black=decoded, blue=actual) ── + fig2, ax2 = plt.subplots(1, 1, figsize=(14, 9)) + ax2.fill_between( + time, result["ci_low"], result["ci_high"], + color="0.75", alpha=0.4, label="95% CI" ) + ax2.plot(time, result["x_decoded"], "k-", linewidth=4, label="Decoded") + ax2.plot(time, stimSignal, "b-", linewidth=4, label="Actual") + ax2.set_xlabel("time [s]") + ax2.set_ylabel("Stimulus") # MATLAB: ylabel('Stimulus','Interpreter','none') + ax2.set_title(f"Decoded Stimulus $\\pm$ 95% CIs with {result['n_cells']} cells", + fontweight="bold", fontsize=18, fontfamily="Arial") + ax2.legend(["Decoded", "Actual"], loc="upper right") fig2.tight_layout() return fig1, fig2 def _plot_part_b(result): - """Figure 3: Reach setup (4×2). Figure 4: 20-sim decoded paths (4×2).""" - time = result["time"] - xState = result["xState"] - setup = result["setup"] + """Figure 3: Reach setup (4×2 layout). Figure 4: Overlaid decoded trajectories.""" + ex = result["example"] + time = ex["time"] + xState = ex["xState"] + dN = ex["dN"] + delta = result["delta"] + n_cells = result["n_cells"] - # ── Figure 3: 4×2 setup — matches MATLAB subplot(4,2,...) ── + # ── Figure 3: Reach trajectory and population setup (4×2 layout) ── fig3 = plt.figure(figsize=(14, 9)) - # (4,2,[1,3]): 2D reach path with start/end markers + # Top-left [1,3]: 2D reach path (in cm) ax_path = fig3.add_subplot(4, 2, (1, 3)) ax_path.plot(100 * xState[0, :], 100 * xState[1, :], "k", linewidth=2) - ax_path.plot(100 * xState[0, 0], 100 * xState[1, 0], "bo", markersize=14, - markerfacecolor="none", markeredgewidth=1.5) - ax_path.plot(100 * xState[0, -1], 100 * xState[1, -1], "ro", markersize=14, - markerfacecolor="none", markeredgewidth=1.5) + ax_path.plot(100 * xState[0, 0], 100 * xState[1, 0], "bo", markersize=14) + ax_path.plot(100 * xState[0, -1], 100 * xState[1, -1], "ro", markersize=14) + ax_path.legend(["Path", "Start", "Finish"], loc="upper right") ax_path.set_xlabel("X Position [cm]") ax_path.set_ylabel("Y Position [cm]") - ax_path.set_title("Reach Path", fontweight="bold", fontsize=14, - fontfamily="Arial") - ax_path.legend(["Path", "Start", "Finish"], loc="upper right") - - # (4,2,[2,4]): Spike raster - ax_raster = fig3.add_subplot(4, 2, (2, 4)) - dN = setup["dN"] - n_cells = setup["n_cells"] - for c in range(n_cells): - spike_t = time[dN[c, :] > 0] - ax_raster.plot(spike_t, np.full_like(spike_t, c + 1), "|", color="k", - markersize=2) - ax_raster.set_ylim(0.5, n_cells + 0.5) - ax_raster.set_xticklabels([]) - ax_raster.set_title("Neural Raster", fontweight="bold", fontsize=14, - fontfamily="Arial") - ax_raster.set_xlabel("time [s]") - ax_raster.set_ylabel("Cell Number") + ax_path.set_title("Reach Path", fontweight="bold", fontsize=14) - # (4,2,5): Position traces x, y + # Middle-left [5]: position vs time (in cm) ax_pos = fig3.add_subplot(4, 2, 5) h1, = ax_pos.plot(time, 100 * xState[0, :], "k", linewidth=2) h2, = ax_pos.plot(time, 100 * xState[1, :], "k-.", linewidth=2) @@ -581,87 +593,83 @@ def _plot_part_b(result): ax_pos.set_xlabel("time [s]") ax_pos.set_ylabel("Position [cm]") - # (4,2,7): Velocity traces vx, vy + # Lower-left [7]: velocity vs time (in cm/s) ax_vel = fig3.add_subplot(4, 2, 7) h1, = ax_vel.plot(time, 100 * xState[2, :], "k", linewidth=2) h2, = ax_vel.plot(time, 100 * xState[3, :], "k-.", linewidth=2) - ax_vel.legend([h1, h2], ["v_x", "v_y"], loc="upper right") + ax_vel.legend([h1, h2], ["$v_x$", "$v_y$"], loc="upper right") ax_vel.set_xlabel("time [s]") ax_vel.set_ylabel("Velocity [cm/s]") - # (4,2,[6,8]): CIFs overlaid in black + # Top-right [2,4]: neural raster + ax_raster = fig3.add_subplot(4, 2, (2, 4)) + for c in range(n_cells): + spike_t = time[dN[c, :] > 0] + ax_raster.plot(spike_t, np.full_like(spike_t, c + 1), "|", color="k", markersize=4) + ax_raster.set_ylabel("Cell Number") + ax_raster.set_xticks([]) + ax_raster.set_xticklabels([]) + ax_raster.set_title("Neural Raster", fontweight="bold", fontsize=14) + + # Bottom-right [6,8]: CIF curves ax_cif = fig3.add_subplot(4, 2, (6, 8)) - lambdaAll = setup["lambdaAll"] + muCoeffs = ex["muCoeffs"] + beta = ex["beta"] for c in range(n_cells): - ax_cif.plot(time, lambdaAll[c, :], "k", linewidth=0.5) + eta = muCoeffs[c] + beta[:, c] @ xState + exp_eta = np.exp(np.clip(eta, -20, 20)) + lam = (exp_eta / (1.0 + exp_eta)) / delta + ax_cif.plot(time, lam, "k", linewidth=0.5) ax_cif.set_title("Neural Conditional Intensity Functions", - fontweight="bold", fontsize=14, fontfamily="Arial") + fontweight="bold", fontsize=14) ax_cif.set_xlabel("time [s]") ax_cif.set_ylabel("Firing Rate [spikes/sec]") fig3.tight_layout() - # ── Figure 4: 4×2 overlaid decoded paths — matches MATLAB ── + # ── Figure 4: Overlaid decoded trajectories (4×2 layout, 20 runs) ── fig4 = plt.figure(figsize=(14, 9)) - all_goal = result["all_x_u_goal"] - all_free = result["all_x_u_free"] - - # (4,2,1:4): 2D reach paths overlaid — MATLAB overlays all sims + Start/Finish - ax_paths = fig4.add_subplot(4, 2, (1, 4)) - ax_paths.plot(100 * xState[0, :], 100 * xState[1, :], "k", linewidth=3) - for k in range(len(all_goal)): - ax_paths.plot(100 * all_goal[k][0, :], 100 * all_goal[k][1, :], "b", - linewidth=0.5, alpha=0.7) - ax_paths.plot(100 * all_free[k][0, :], 100 * all_free[k][1, :], "g", - linewidth=0.5, alpha=0.7) - hStart, = ax_paths.plot(100 * xState[0, 0], 100 * xState[1, 0], "bo", - markersize=14, markerfacecolor="none", markeredgewidth=1.5) - hFinish, = ax_paths.plot(100 * xState[0, -1], 100 * xState[1, -1], "ro", - markersize=14, markerfacecolor="none", markeredgewidth=1.5) - ax_paths.legend([hStart, hFinish], ["Start", "Finish"], loc="upper right") - ax_paths.set_title("Estimated vs. Actual Reach Paths", fontweight="bold", - fontsize=12, fontfamily="Arial") - ax_paths.set_xlabel("x [cm]") - ax_paths.set_ylabel("y [cm]") - - # (4,2,5): x(t) - ax_x = fig4.add_subplot(4, 2, 5) - ax_x.plot(time, 100 * xState[0, :], "k", linewidth=3) - for k in range(len(all_goal)): - ax_x.plot(time, 100 * all_goal[k][0, :], "b", linewidth=0.5, alpha=0.5) - ax_x.plot(time, 100 * all_free[k][0, :], "g", linewidth=0.5, alpha=0.5) - ax_x.set_ylabel("x(t) [cm]") - ax_x.set_xticklabels([]) - - # (4,2,6): y(t) with legend - ax_y = fig4.add_subplot(4, 2, 6) - hA, = ax_y.plot(time, 100 * xState[1, :], "k", linewidth=3) - hB, = ax_y.plot(time, 100 * all_goal[0][1, :], "b", linewidth=0.5) - hC, = ax_y.plot(time, 100 * all_free[0][1, :], "g", linewidth=0.5) - for k in range(1, len(all_goal)): - ax_y.plot(time, 100 * all_goal[k][1, :], "b", linewidth=0.5, alpha=0.5) - ax_y.plot(time, 100 * all_free[k][1, :], "g", linewidth=0.5, alpha=0.5) - ax_y.legend([hA, hB, hC], ["Actual", "PPAF+Goal", "PPAF"], loc="lower right") - ax_y.set_ylabel("y(t) [cm]") - ax_y.set_xticklabels([]) - # (4,2,7): vx(t) - ax_vx = fig4.add_subplot(4, 2, 7) - ax_vx.plot(time, 100 * xState[2, :], "k", linewidth=3) - for k in range(len(all_goal)): - ax_vx.plot(time, 100 * all_goal[k][2, :], "b", linewidth=0.5, alpha=0.5) - ax_vx.plot(time, 100 * all_free[k][2, :], "g", linewidth=0.5, alpha=0.5) - ax_vx.set_ylabel("v_x(t) [cm/s]") - ax_vx.set_xlabel("time [s]") + # Top [1:4]: 2D estimated vs actual reach paths + ax_2d = fig4.add_subplot(4, 2, (1, 4)) + ax_2d.plot(100 * xState[0, :], 100 * xState[1, :], "k", linewidth=3) + ax_2d.set_title("Estimated vs. Actual Reach Paths", + fontweight="bold", fontsize=12) + + for sim_idx in range(result["n_sims"]): + x_u_goal = result["all_runs_goal"][sim_idx] + x_u_free = result["all_runs_free"][sim_idx] + ax_2d.plot(100 * x_u_goal[0, :], 100 * x_u_goal[1, :], "b", linewidth=0.5) + ax_2d.plot(100 * x_u_free[0, :], 100 * x_u_free[1, :], "g", linewidth=0.5) + ax_2d.set_xlabel("x [cm]") + ax_2d.set_ylabel("y [cm]") - # (4,2,8): vy(t) - ax_vy = fig4.add_subplot(4, 2, 8) - ax_vy.plot(time, 100 * xState[3, :], "k", linewidth=3) - for k in range(len(all_goal)): - ax_vy.plot(time, 100 * all_goal[k][3, :], "b", linewidth=0.5, alpha=0.5) - ax_vy.plot(time, 100 * all_free[k][3, :], "g", linewidth=0.5, alpha=0.5) - ax_vy.set_ylabel("v_y(t) [cm/s]") - ax_vy.set_xlabel("time [s]") + # Bottom panels: per-state traces + state_labels = ["x(t) [cm]", "y(t) [cm]", "$v_x$(t) [cm/s]", "$v_y$(t) [cm/s]"] + subplot_indices = [5, 6, 7, 8] + scale = 100.0 # meters → cm + + for d, (sp_idx, ylabel) in enumerate(zip(subplot_indices, state_labels)): + ax = fig4.add_subplot(4, 2, sp_idx) + ax.plot(time, scale * xState[d, :], "k", linewidth=3) + + for sim_idx in range(result["n_sims"]): + x_u_goal = result["all_runs_goal"][sim_idx] + x_u_free = result["all_runs_free"][sim_idx] + hB, = ax.plot(time, scale * x_u_goal[d, :], "b", linewidth=0.5) + hC, = ax.plot(time, scale * x_u_free[d, :], "g", linewidth=0.5) + + ax.set_ylabel(ylabel) + if d >= 2: + ax.set_xlabel("time [s]") + else: + ax.set_xticklabels([]) + + # Add legend on y(t) panel (subplot 6), matching MATLAB + if d == 1: + hA, = ax.plot([], [], "k", linewidth=3) + ax.legend([hA, hB, hC], ["Actual", "PPAF+Goal", "PPAF"], + loc="lower right", fontsize=8) fig4.tight_layout() @@ -669,44 +677,26 @@ def _plot_part_b(result): def _plot_part_c(result): - """Figure 5: Hybrid setup (4×2). Figure 6: 20-sim decode (4×3).""" + """Figure 5: Hybrid setup (4×2 layout). Figure 6: Hybrid decoding summary (4×3).""" time = result["time"] - X = result["X"] + X = result["X"] # (6, T) mstate = result["mstate"] - setup = result["setup"] + dN = result["dN"] + n_cells = result["n_cells"] - # ── Figure 5: 4×2 setup — matches MATLAB subplot(4,2,...) ── + # ── Figure 5: Setup — reach path, traces, raster, discrete state (4×2) ── fig5 = plt.figure(figsize=(14, 9)) - # (4,2,[1,3]): Reach path with markers — MATLAB uses unfilled circles + legend + # Top-left [1,3]: 2D reach path ax_path = fig5.add_subplot(4, 2, (1, 3)) ax_path.plot(100 * X[0, :], 100 * X[1, :], "k", linewidth=2) - hStart, = ax_path.plot(100 * X[0, 0], 100 * X[1, 0], "bo", markersize=16, - markerfacecolor="none", markeredgewidth=1.5) - hFinish, = ax_path.plot(100 * X[0, -1], 100 * X[1, -1], "ro", markersize=16, - markerfacecolor="none", markeredgewidth=1.5) - ax_path.legend([hStart, hFinish], ["Start", "Finish"], loc="upper right") + ax_path.plot(100 * X[0, 0], 100 * X[1, 0], "bo", markersize=16) + ax_path.plot(100 * X[0, -1], 100 * X[1, -1], "ro", markersize=16) ax_path.set_xlabel("X [cm]") ax_path.set_ylabel("Y [cm]") - ax_path.set_title("Reach Path", fontweight="bold", fontsize=14, - fontfamily="Arial") - - # (4,2,[2,4]): Spike raster - ax_raster = fig5.add_subplot(4, 2, (2, 4)) - dN = setup["dN"] - n_cells = setup["n_cells"] - for c in range(n_cells): - spike_t = time[dN[c, :] > 0] - ax_raster.plot(spike_t, np.full_like(spike_t, c + 1), "|", color="k", - markersize=2) - ax_raster.set_ylim(0.5, n_cells + 0.5) - ax_raster.set_xticklabels([]) - ax_raster.set_title("Neural Raster", fontweight="bold", fontsize=14, - fontfamily="Arial") - ax_raster.set_xlabel("time [s]") - ax_raster.set_ylabel("Cell Number") + ax_path.set_title("Reach Path", fontweight="bold", fontsize=14) - # (4,2,5): Position traces + # Middle-left [5]: position vs time ax_pos = fig5.add_subplot(4, 2, 5) h1, = ax_pos.plot(time, 100 * X[0, :], "k", linewidth=2) h2, = ax_pos.plot(time, 100 * X[1, :], "k-.", linewidth=2) @@ -714,15 +704,26 @@ def _plot_part_c(result): ax_pos.set_xlabel("time [s]") ax_pos.set_ylabel("Position [cm]") - # (4,2,7): Velocity traces + # Lower-left [7]: velocity vs time ax_vel = fig5.add_subplot(4, 2, 7) h1, = ax_vel.plot(time, 100 * X[2, :], "k", linewidth=2) h2, = ax_vel.plot(time, 100 * X[3, :], "k-.", linewidth=2) - ax_vel.legend([h1, h2], ["v_x", "v_y"], loc="upper right") + ax_vel.legend([h1, h2], ["$v_x$", "$v_y$"], loc="upper right") ax_vel.set_xlabel("time [s]") ax_vel.set_ylabel("Velocity [cm/s]") - # (4,2,[6,8]): Discrete movement state + # Top-right [2,4]: neural raster (show ALL cells, matching MATLAB) + ax_raster = fig5.add_subplot(4, 2, (2, 4)) + for c in range(dN.shape[0]): + spike_t = time[dN[c, :] > 0] + ax_raster.plot(spike_t, np.full_like(spike_t, c + 1), "|", color="k", markersize=4) + ax_raster.set_ylabel("Cell Number") + ax_raster.set_yticklabels([]) + ax_raster.set_xticks([]) + ax_raster.set_xticklabels([]) + ax_raster.set_title("Neural Raster", fontweight="bold", fontsize=14) + + # Bottom-right [6,8]: discrete movement state ax_state = fig5.add_subplot(4, 2, (6, 8)) ax_state.plot(time, mstate, "k", linewidth=2) ax_state.set_ylim(0, 3) @@ -730,112 +731,93 @@ def _plot_part_c(result): ax_state.set_yticklabels(["N", "M"]) ax_state.set_xlabel("time [s]") ax_state.set_ylabel("state") - ax_state.set_title("Discrete Movement State", fontweight="bold", - fontsize=14, fontfamily="Arial") + ax_state.set_title("Discrete Movement State", fontweight="bold", fontsize=14) fig5.tight_layout() - # ── Figure 6: 4×3 decode — matches MATLAB example05_decoding_ppaf_pphf.m ── - # MATLAB example05 plots ONLY means (thick solid lines), no individual traces. + # ── Figure 6: Hybrid decoding results (4×3 layout, averaged over 20 sims) ── fig6 = plt.figure(figsize=(14, 9)) - S_estAll = result["S_estAll"] # (n_sims, T) - S_estNTAll = result["S_estNTAll"] - MU_estAll = result["MU_estAll"] # (n_sims, n_modes, T) - MU_estNTAll = result["MU_estNTAll"] - X_estAll = result["X_estAll"] # (6, T, n_sims) - X_estNTAll = result["X_estNTAll"] + # Mean across simulations + mS_est = np.mean(result["S_estAll"], axis=0) + mS_estNT = np.mean(result["S_estNTAll"], axis=0) + mMU_est = np.mean(result["MU_estAll"][1, :, :], axis=1) # P(M|data) for goal + mMU_estNT = np.mean(result["MU_estNTAll"][1, :, :], axis=1) # P(M|data) for free + mX_est = np.mean(100 * result["X_estAll"], axis=2) + mX_estNT = np.mean(100 * result["X_estNTAll"], axis=2) - # Pre-create all subplots + # Left column: state estimation + probability + # [1,4]: Estimated vs actual state ax_s = fig6.add_subplot(4, 3, (1, 4)) - ax_p = fig6.add_subplot(4, 3, (7, 10)) - ax_2d = fig6.add_subplot(4, 3, (2, 6)) - ax_xp = fig6.add_subplot(4, 3, 8) - ax_yp = fig6.add_subplot(4, 3, 9) - ax_xv = fig6.add_subplot(4, 3, 11) - ax_yv = fig6.add_subplot(4, 3, 12) - - # --- Mean traces (thick solid — MATLAB plots only means, no individual) --- - mS_est = np.mean(S_estAll, axis=0) - mS_estNT = np.mean(S_estNTAll, axis=0) - mMU_est = np.mean(MU_estAll, axis=0) - mMU_estNT = np.mean(MU_estNTAll, axis=0) - mX_est = 100 * np.mean(X_estAll, axis=2) - mX_estNT = 100 * np.mean(X_estNTAll, axis=2) - - # (4,3,[1,4]): State ax_s.plot(time, mstate, "k", linewidth=3) ax_s.plot(time, mS_est, "b", linewidth=3) ax_s.plot(time, mS_estNT, "g", linewidth=3) - ax_s.set_xticklabels([]) - ax_s.set_ylim(0.5, 2.5) ax_s.set_yticks([1, 2.1]) ax_s.set_yticklabels(["N", "M"]) + ax_s.set_xticklabels([]) ax_s.set_ylabel("state") - ax_s.set_title("Estimated vs. Actual State", fontweight="bold", - fontsize=12, fontfamily="Arial") - - # (4,3,[7,10]): P(s=M|data) - ax_p.plot(time, mMU_est[1, :], "b", linewidth=3) - ax_p.plot(time, mMU_estNT[1, :], "g", linewidth=3) - ax_p.set_xlim(time[0], time[-1]) - ax_p.set_ylim(0, 1.1) - ax_p.set_xlabel("time [s]") - ax_p.set_ylabel("P(s(t)=M | data)") - ax_p.set_title("Probability of State", fontweight="bold", fontsize=12, - fontfamily="Arial") - - # (4,3,[2,3,5,6]): 2D reach — actual + mean + Start/Finish + ax_s.set_title("Estimated vs. Actual State", fontweight="bold", fontsize=12) + + # [7,10]: P(s(t)=M | data) + ax_prob = fig6.add_subplot(4, 3, (7, 10)) + ax_prob.plot(time, mMU_est, "b", linewidth=3) + ax_prob.plot(time, mMU_estNT, "g", linewidth=3) + ax_prob.set_xlim(time[0], time[-1]) + ax_prob.set_ylim(0, 1.1) + ax_prob.set_xlabel("time [s]") + ax_prob.set_ylabel("P(s(t)=M | data)") + ax_prob.set_title("Probability of State", fontweight="bold", fontsize=12) + + # Right top [2,3,5,6]: 2D estimated vs actual reach path + ax_2d = fig6.add_subplot(4, 3, (2, 6)) ax_2d.plot(100 * X[0, :], 100 * X[1, :], "k", linewidth=1) ax_2d.plot(mX_est[0, :], mX_est[1, :], "b", linewidth=3) ax_2d.plot(mX_estNT[0, :], mX_estNT[1, :], "g", linewidth=3) - hStart, = ax_2d.plot(100 * X[0, 0], 100 * X[1, 0], "bo", markersize=14, - markerfacecolor="none", markeredgewidth=1.5) - hFinish, = ax_2d.plot(100 * X[0, -1], 100 * X[1, -1], "ro", markersize=14, - markerfacecolor="none", markeredgewidth=1.5) - ax_2d.legend([hStart, hFinish], ["Start", "Finish"], loc="upper right") + ax_2d.plot(100 * X[0, 0], 100 * X[1, 0], "bo", markersize=14) + ax_2d.plot(100 * X[0, -1], 100 * X[1, -1], "ro", markersize=14) ax_2d.set_xlabel("x [cm]") ax_2d.set_ylabel("y [cm]") - ax_2d.set_title("Estimated vs. Actual Reach Path", fontweight="bold", - fontsize=12, fontfamily="Arial") - - # (4,3,8): X position - ax_xp.plot(time, 100 * X[0, :], "k", linewidth=3) - ax_xp.plot(time, mX_est[0, :], "b", linewidth=3) - ax_xp.plot(time, mX_estNT[0, :], "g", linewidth=3) - ax_xp.set_ylabel("x(t) [cm]") - ax_xp.set_xticklabels([]) - ax_xp.set_title("X Position", fontweight="bold", fontsize=12, - fontfamily="Arial") - - # (4,3,9): Y position with legend - h1, = ax_yp.plot(time, 100 * X[1, :], "k", linewidth=3) - h2, = ax_yp.plot(time, mX_est[1, :], "b", linewidth=3) - h3, = ax_yp.plot(time, mX_estNT[1, :], "g", linewidth=3) - ax_yp.legend([h1, h2, h3], ["Actual", "PPAF+Goal", "PPAF"], - loc="lower right") - ax_yp.set_ylabel("y(t) [cm]") - ax_yp.set_xticklabels([]) - ax_yp.set_title("Y Position", fontweight="bold", fontsize=12, - fontfamily="Arial") - - # (4,3,11): X velocity - ax_xv.plot(time, 100 * X[2, :], "k", linewidth=3) - ax_xv.plot(time, mX_est[2, :], "b", linewidth=3) - ax_xv.plot(time, mX_estNT[2, :], "g", linewidth=3) - ax_xv.set_ylabel("v_x(t) [cm/s]") - ax_xv.set_xlabel("time [s]") - ax_xv.set_title("X Velocity", fontweight="bold", fontsize=12, - fontfamily="Arial") - - # (4,3,12): Y velocity - ax_yv.plot(time, 100 * X[3, :], "k", linewidth=3) - ax_yv.plot(time, mX_est[3, :], "b", linewidth=3) - ax_yv.plot(time, mX_estNT[3, :], "g", linewidth=3) - ax_yv.set_ylabel("v_y(t) [cm/s]") - ax_yv.set_xlabel("time [s]") - ax_yv.set_title("Y Velocity", fontweight="bold", fontsize=12, - fontfamily="Arial") + ax_2d.set_title("Estimated vs. Actual Reach Path", + fontweight="bold", fontsize=12) + + # Bottom panels: per-state traces + # [8]: x(t) + ax_x = fig6.add_subplot(4, 3, 8) + ax_x.plot(time, 100 * X[0, :], "k", linewidth=3) + ax_x.plot(time, mX_est[0, :], "b", linewidth=3) + ax_x.plot(time, mX_estNT[0, :], "g", linewidth=3) + ax_x.set_ylabel("x(t) [cm]") + ax_x.set_xticklabels([]) + ax_x.set_title("X Position", fontweight="bold", fontsize=12) + + # [9]: y(t) with legend + ax_y = fig6.add_subplot(4, 3, 9) + h1, = ax_y.plot(time, 100 * X[1, :], "k", linewidth=3) + h2, = ax_y.plot(time, mX_est[1, :], "b", linewidth=3) + h3, = ax_y.plot(time, mX_estNT[1, :], "g", linewidth=3) + ax_y.legend([h1, h2, h3], ["Actual", "PPAF+Goal", "PPAF"], + loc="lower right", fontsize=8) + ax_y.set_ylabel("y(t) [cm]") + ax_y.set_xticklabels([]) + ax_y.set_title("Y Position", fontweight="bold", fontsize=12) + + # [11]: vx(t) + ax_vx = fig6.add_subplot(4, 3, 11) + ax_vx.plot(time, 100 * X[2, :], "k", linewidth=3) + ax_vx.plot(time, mX_est[2, :], "b", linewidth=3) + ax_vx.plot(time, mX_estNT[2, :], "g", linewidth=3) + ax_vx.set_ylabel("$v_x$(t) [cm/s]") + ax_vx.set_xlabel("time [s]") + ax_vx.set_title("X Velocity", fontweight="bold", fontsize=12) + + # [12]: vy(t) + ax_vy = fig6.add_subplot(4, 3, 12) + ax_vy.plot(time, 100 * X[3, :], "k", linewidth=3) + ax_vy.plot(time, mX_est[3, :], "b", linewidth=3) + ax_vy.plot(time, mX_estNT[3, :], "g", linewidth=3) + ax_vy.set_ylabel("$v_y$(t) [cm/s]") + ax_vy.set_xlabel("time [s]") + ax_vy.set_title("Y Velocity", fontweight="bold", fontsize=12) fig6.tight_layout() @@ -856,15 +838,16 @@ def run_example05(*, export_figures=False, export_dir=None, show=False): 1. 20-cell sinusoidal-tuned population, binomial CIF. 2. PPDecodeFilterLinear decoding with 95% CIs. - Part B — Arm-reach PPAF (Figs 3–4): - 3. Minimum-energy reaching trajectory (4-D state). - 4. Velocity-tuned 20-cell population, binomial CIF. - 5. 20 simulations: PPAF free vs PPAF+Goal, overlaid decoded paths. + Part B — Arm-reach PPAF: + 4. Simulate 4-state minimum-jerk reaching movement. + 5. Encode with 20-cell velocity-tuned population. + 6. Decode with PPAF (free) and PPAF+Goal; 20 overlaid simulations. - Part C — Hybrid filter (Figs 5–6): - 6. Fixture trajectory with 2 discrete movement states. - 7. 40-cell velocity-tuned population, binomial CIF. - 8. 20 simulations: PPHybridFilterLinear, averaged decode results. + Part C — Hybrid filter: + 7. Load 6-state fixture trajectory with 2 discrete modes. + 8. Simulate 40-cell population with velocity tuning. + 9. Decode joint discrete/continuous state via PPHybridFilterLinear + (both goal-directed and free), averaged over 20 simulations. """ print("=" * 70) print("Example 05: Stimulus Decoding with PPAF and PPHF") @@ -878,13 +861,12 @@ def run_example05(*, export_figures=False, export_dir=None, show=False): # --- Part B --- print("\n--- Part B: Arm Reach PPAF (20 simulations) ---") result_b = _run_part_b() - print(f" {result_b['n_cells']} cells, {result_b['n_sims']} sims completed") + print(f" {result_b['n_sims']} simulations, {result_b['n_cells']} cells") - # --- Part C --- + # --- Part C: Hybrid filter --- print("\n--- Part C: Hybrid Filter (20 simulations) ---") result_c = _run_part_c() - print(f" {result_c['n_cells']} cells, state accuracy = {result_c['state_acc']:.1%}") - print(f" Position RMSE: x={result_c['rmse_x']:.4f}, y={result_c['rmse_y']:.4f}") + print(f" {result_c['n_cells']} cells, {result_c['n_sims']} simulations") # Summary summary = { @@ -898,9 +880,7 @@ def run_example05(*, export_figures=False, export_dir=None, show=False): }, "experiment6": { "num_cells": float(result_c["n_cells"]), - "state_accuracy": result_c["state_acc"], - "decode_rmse_x": result_c["rmse_x"], - "decode_rmse_y": result_c["rmse_y"], + "n_sims": float(result_c["n_sims"]), }, } print("\n" + json.dumps(summary, indent=2)) diff --git a/examples/paper/figures/example01/fig01_constant_mg_summary.png b/examples/paper/figures/example01/fig01_constant_mg_summary.png new file mode 100644 index 00000000..298e5ebf Binary files /dev/null and b/examples/paper/figures/example01/fig01_constant_mg_summary.png differ diff --git a/examples/paper/figures/example01/fig02_washout_raster_overview.png b/examples/paper/figures/example01/fig02_washout_raster_overview.png new file mode 100644 index 00000000..a3f02e6f Binary files /dev/null and b/examples/paper/figures/example01/fig02_washout_raster_overview.png differ diff --git a/examples/paper/figures/example01/fig03_piecewise_baseline_comparison.png b/examples/paper/figures/example01/fig03_piecewise_baseline_comparison.png new file mode 100644 index 00000000..45397bdb Binary files /dev/null and b/examples/paper/figures/example01/fig03_piecewise_baseline_comparison.png differ diff --git a/examples/paper/figures/example02/fig01_data_overview.png b/examples/paper/figures/example02/fig01_data_overview.png new file mode 100644 index 00000000..63823417 Binary files /dev/null and b/examples/paper/figures/example02/fig01_data_overview.png differ diff --git a/examples/paper/figures/example02/fig02_lag_and_model_comparison.png b/examples/paper/figures/example02/fig02_lag_and_model_comparison.png new file mode 100644 index 00000000..748a2175 Binary files /dev/null and b/examples/paper/figures/example02/fig02_lag_and_model_comparison.png differ diff --git a/examples/paper/figures/example03/fig01_simulated_and_real_rasters.png b/examples/paper/figures/example03/fig01_simulated_and_real_rasters.png new file mode 100644 index 00000000..4ac7fb64 Binary files /dev/null and b/examples/paper/figures/example03/fig01_simulated_and_real_rasters.png differ diff --git a/examples/paper/figures/example03/fig02_psth_comparison.png b/examples/paper/figures/example03/fig02_psth_comparison.png new file mode 100644 index 00000000..68725db6 Binary files /dev/null and b/examples/paper/figures/example03/fig02_psth_comparison.png differ diff --git a/examples/paper/figures/example03/fig03_ssglm_simulation_summary.png b/examples/paper/figures/example03/fig03_ssglm_simulation_summary.png new file mode 100644 index 00000000..f559d76c Binary files /dev/null and b/examples/paper/figures/example03/fig03_ssglm_simulation_summary.png differ diff --git a/examples/paper/figures/example03/fig04_ssglm_fit_diagnostics.png b/examples/paper/figures/example03/fig04_ssglm_fit_diagnostics.png new file mode 100644 index 00000000..9542f35c Binary files /dev/null and b/examples/paper/figures/example03/fig04_ssglm_fit_diagnostics.png differ diff --git a/examples/paper/figures/example03/fig05_stimulus_effect_surfaces.png b/examples/paper/figures/example03/fig05_stimulus_effect_surfaces.png new file mode 100644 index 00000000..7c88841e Binary files /dev/null and b/examples/paper/figures/example03/fig05_stimulus_effect_surfaces.png differ diff --git a/examples/paper/figures/example03/fig06_learning_trial_comparison.png b/examples/paper/figures/example03/fig06_learning_trial_comparison.png new file mode 100644 index 00000000..36f32429 Binary files /dev/null and b/examples/paper/figures/example03/fig06_learning_trial_comparison.png differ diff --git a/examples/paper/figures/example04/fig01_example_cells_path_overlay.png b/examples/paper/figures/example04/fig01_example_cells_path_overlay.png new file mode 100644 index 00000000..89a4491b Binary files /dev/null and b/examples/paper/figures/example04/fig01_example_cells_path_overlay.png differ diff --git a/examples/paper/figures/example04/fig02_model_summary_statistics.png b/examples/paper/figures/example04/fig02_model_summary_statistics.png new file mode 100644 index 00000000..5d558144 Binary files /dev/null and b/examples/paper/figures/example04/fig02_model_summary_statistics.png differ diff --git a/examples/paper/figures/example04/fig03_gaussian_place_fields_animal1.png b/examples/paper/figures/example04/fig03_gaussian_place_fields_animal1.png new file mode 100644 index 00000000..57f2cfe2 Binary files /dev/null and b/examples/paper/figures/example04/fig03_gaussian_place_fields_animal1.png differ diff --git a/examples/paper/figures/example04/fig04_zernike_place_fields_animal1.png b/examples/paper/figures/example04/fig04_zernike_place_fields_animal1.png new file mode 100644 index 00000000..c51a9996 Binary files /dev/null and b/examples/paper/figures/example04/fig04_zernike_place_fields_animal1.png differ diff --git a/examples/paper/figures/example04/fig05_gaussian_place_fields_animal2.png b/examples/paper/figures/example04/fig05_gaussian_place_fields_animal2.png new file mode 100644 index 00000000..0974d97a Binary files /dev/null and b/examples/paper/figures/example04/fig05_gaussian_place_fields_animal2.png differ diff --git a/examples/paper/figures/example04/fig06_zernike_place_fields_animal2.png b/examples/paper/figures/example04/fig06_zernike_place_fields_animal2.png new file mode 100644 index 00000000..4e76d94d Binary files /dev/null and b/examples/paper/figures/example04/fig06_zernike_place_fields_animal2.png differ diff --git a/examples/paper/figures/example04/fig07_example_cell_mesh_comparison.png b/examples/paper/figures/example04/fig07_example_cell_mesh_comparison.png new file mode 100644 index 00000000..b7c5d750 Binary files /dev/null and b/examples/paper/figures/example04/fig07_example_cell_mesh_comparison.png differ diff --git a/examples/paper/figures/example05/fig01_univariate_setup.png b/examples/paper/figures/example05/fig01_univariate_setup.png new file mode 100644 index 00000000..b7d538de Binary files /dev/null and b/examples/paper/figures/example05/fig01_univariate_setup.png differ diff --git a/examples/paper/figures/example05/fig02_univariate_decoding.png b/examples/paper/figures/example05/fig02_univariate_decoding.png new file mode 100644 index 00000000..b8e96a65 Binary files /dev/null and b/examples/paper/figures/example05/fig02_univariate_decoding.png differ diff --git a/examples/paper/figures/example05/fig03_reach_and_population_setup.png b/examples/paper/figures/example05/fig03_reach_and_population_setup.png new file mode 100644 index 00000000..f10bc678 Binary files /dev/null and b/examples/paper/figures/example05/fig03_reach_and_population_setup.png differ diff --git a/examples/paper/figures/example05/fig04_ppaf_goal_vs_free.png b/examples/paper/figures/example05/fig04_ppaf_goal_vs_free.png new file mode 100644 index 00000000..dda658cb Binary files /dev/null and b/examples/paper/figures/example05/fig04_ppaf_goal_vs_free.png differ diff --git a/examples/paper/figures/example05/fig05_hybrid_setup.png b/examples/paper/figures/example05/fig05_hybrid_setup.png new file mode 100644 index 00000000..0c53e806 Binary files /dev/null and b/examples/paper/figures/example05/fig05_hybrid_setup.png differ diff --git a/examples/paper/figures/example05/fig06_hybrid_decoding_summary.png b/examples/paper/figures/example05/fig06_hybrid_decoding_summary.png new file mode 100644 index 00000000..1b23486c Binary files /dev/null and b/examples/paper/figures/example05/fig06_hybrid_decoding_summary.png differ diff --git a/notebooks/AnalysisExamples.ipynb b/notebooks/AnalysisExamples.ipynb index 68f00805..b274b80f 100644 --- a/notebooks/AnalysisExamples.ipynb +++ b/notebooks/AnalysisExamples.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "1bf27b7c", - "metadata": {}, + "execution_count": 1, + "id": "7807842d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:27.936748Z", + "iopub.status.busy": "2026-03-14T15:32:27.936516Z", + "iopub.status.idle": "2026-03-14T15:32:30.740702Z", + "shell.execute_reply": "2026-03-14T15:32:30.740115Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: AnalysisExamples\n", @@ -76,10 +83,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "39e80ef9", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "37ac20c9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.742159Z", + "iopub.status.busy": "2026-03-14T15:32:30.742008Z", + "iopub.status.idle": "2026-03-14T15:32:30.744154Z", + "shell.execute_reply": "2026-03-14T15:32:30.743787Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'n_samples': 41348, 'n_spikes': 2614, 'sample_rate_hz': 30.0}\n" + ] + } + ], "source": [ "# SECTION 1: Analysis Examples\n", "plt.close(\"all\")\n", @@ -88,9 +110,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "b2e4c2dc", - "metadata": {}, + "execution_count": 3, + "id": "dbdc74f9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.745260Z", + "iopub.status.busy": "2026-03-14T15:32:30.745181Z", + "iopub.status.idle": "2026-03-14T15:32:30.766757Z", + "shell.execute_reply": "2026-03-14T15:32:30.766340Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Example 1: Tradition Preliminary Analysis\n", @@ -112,10 +141,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "15ae5117", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "5c38cff1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.768114Z", + "iopub.status.busy": "2026-03-14T15:32:30.768031Z", + "iopub.status.idle": "2026-03-14T15:32:30.788529Z", + "shell.execute_reply": "2026-03-14T15:32:30.788068Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Rat trajectory with spike locations')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 3: visualize the raw data\n", "fig = _prepare_figure(\"figure; plot(xN,yN,x_at_spiketimes,y_at_spiketimes,'r.')\", figsize=(6.5, 6.0))\n", @@ -130,10 +177,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a8839159", - "metadata": {}, - "outputs": [], + "execution_count": 5, + "id": "5af52914", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.789592Z", + "iopub.status.busy": "2026-03-14T15:32:30.789510Z", + "iopub.status.idle": "2026-03-14T15:32:30.909629Z", + "shell.execute_reply": "2026-03-14T15:32:30.909197Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Quadratic GLM coefficients')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: fit a GLM model to the x and y positions\n", "fig = _prepare_figure(\"figure; errorbar(1:length(b), b, stats.se,'.')\", figsize=(7.0, 4.5))\n", @@ -149,10 +214,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "6bda2bd9", - "metadata": {}, - "outputs": [], + "execution_count": 6, + "id": "5cac7309", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.910876Z", + "iopub.status.busy": "2026-03-14T15:32:30.910794Z", + "iopub.status.idle": "2026-03-14T15:32:30.986012Z", + "shell.execute_reply": "2026-03-14T15:32:30.985609Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0.92, 'Quadratic GLM spatial intensity')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: visualize your model\n", "fig = _prepare_figure(\"figure; mesh(x_new,y_new,lambda,'AlphaData',0)\", figsize=(8.0, 6.5))\n", @@ -174,10 +257,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "0d526433", - "metadata": {}, - "outputs": [], + "execution_count": 7, + "id": "36dd8e70", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.987111Z", + "iopub.status.busy": "2026-03-14T15:32:30.987031Z", + "iopub.status.idle": "2026-03-14T15:32:30.989984Z", + "shell.execute_reply": "2026-03-14T15:32:30.989620Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'linear_mean_rate_hz': 1.8966, 'quadratic_mean_rate_hz': 1.8966}\n" + ] + } + ], "source": [ "# SECTION 6: Compare a linear model versus a Gaussian GLM model\n", "lambda_linear_hz = linear_fit.predict_rate(x_linear) * sample_rate\n", @@ -194,9 +292,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "59472d54", - "metadata": {}, + "execution_count": 8, + "id": "2d8b81fd", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:30.990860Z", + "iopub.status.busy": "2026-03-14T15:32:30.990794Z", + "iopub.status.idle": "2026-03-14T15:32:33.490774Z", + "shell.execute_reply": "2026-03-14T15:32:33.490300Z" + } + }, "outputs": [], "source": [ "# SECTION 7: Make the KS Plot\n", @@ -226,7 +331,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 4, diff --git a/notebooks/AnalysisExamples2.ipynb b/notebooks/AnalysisExamples2.ipynb index 6a21b2a0..a9b1d742 100644 --- a/notebooks/AnalysisExamples2.ipynb +++ b/notebooks/AnalysisExamples2.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "f4ecd812", - "metadata": {}, + "execution_count": 1, + "id": "62e21501", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:43.427073Z", + "iopub.status.busy": "2026-03-14T15:32:43.426769Z", + "iopub.status.idle": "2026-03-14T15:32:45.401143Z", + "shell.execute_reply": "2026-03-14T15:32:45.400597Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: AnalysisExamples2\n", @@ -72,10 +79,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "4dd70d35", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "1836e297", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:45.402632Z", + "iopub.status.busy": "2026-03-14T15:32:45.402504Z", + "iopub.status.idle": "2026-03-14T15:32:45.404599Z", + "shell.execute_reply": "2026-03-14T15:32:45.404221Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'n_samples': 41348, 'n_spikes': 2614, 'analysis_sample_rate_hz': 1000.0}\n" + ] + } + ], "source": [ "# SECTION 1: Analysis Examples 2\n", "plt.close(\"all\")\n", @@ -84,10 +106,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "da89b88a", - "metadata": {}, - "outputs": [], + "execution_count": 3, + "id": "bf657cd0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:45.405603Z", + "iopub.status.busy": "2026-03-14T15:32:45.405536Z", + "iopub.status.idle": "2026-03-14T15:32:45.407326Z", + "shell.execute_reply": "2026-03-14T15:32:45.407011Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'position_shape': [41348, 2], 'velocity_shape': [41348, 2], 'radial_shape': [41348, 5]}\n" + ] + } + ], "source": [ "# SECTION 2: load the rat trajectory and spiking data\n", "print({\"position_shape\": list(position.data.shape), \"velocity_shape\": list(velocity.data.shape), \"radial_shape\": list(radial.data.shape)})\n" @@ -95,10 +132,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "4346a9a7", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "fe47aacc", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:45.408309Z", + "iopub.status.busy": "2026-03-14T15:32:45.408238Z", + "iopub.status.idle": "2026-03-14T15:32:45.410083Z", + "shell.execute_reply": "2026-03-14T15:32:45.409748Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'direct_spike_position_head': [[-0.8536, 0.1573], [-0.666, 0.5866], [-0.6406, 0.6071]], 'upsampled_spike_position_head': [[-0.855, 0.1541], [-0.6636, 0.5889], [-0.6389, 0.6084]]}\n" + ] + } + ], "source": [ "# SECTION 3: interpolate the covariates at the spike times\n", "print(\n", @@ -111,10 +163,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "d5785121", - "metadata": {}, - "outputs": [], + "execution_count": 5, + "id": "2f28e3be", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:45.411119Z", + "iopub.status.busy": "2026-03-14T15:32:45.411048Z", + "iopub.status.idle": "2026-03-14T15:32:45.428209Z", + "shell.execute_reply": "2026-03-14T15:32:45.427818Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Trajectory and interpolated spike locations')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: visualize the raw data\n", "fig = _prepare_figure(\"figure; plot(position.getSubSignal('x').dataToMatrix,...)\", figsize=(6.5, 6.0))\n", @@ -129,9 +199,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "6cf72911", - "metadata": {}, + "execution_count": 6, + "id": "d40c40c9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:45.429253Z", + "iopub.status.busy": "2026-03-14T15:32:45.429186Z", + "iopub.status.idle": "2026-03-14T15:32:50.075260Z", + "shell.execute_reply": "2026-03-14T15:32:50.074822Z" + } + }, "outputs": [], "source": [ "# SECTION 5: Create a trial object and define the fits that we want to run\n", @@ -148,10 +225,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e3b67df9", - "metadata": {}, - "outputs": [], + "execution_count": 7, + "id": "f9160bdb", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:32:50.076746Z", + "iopub.status.busy": "2026-03-14T15:32:50.076667Z", + "iopub.status.idle": "2026-03-14T15:34:22.167697Z", + "shell.execute_reply": "2026-03-14T15:34:22.167307Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'config_names': ['Linear', 'Quadratic', 'Quadratic+Hist'], 'aic': [31007.819, 30961.784, 30960.663]}\n" + ] + } + ], "source": [ "# SECTION 6: Create our collection of configurations and run the analysis\n", "fitResults = Analysis.RunAnalysisForAllNeurons(trial, tcc, 0)\n", @@ -162,10 +254,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "79879555", - "metadata": {}, - "outputs": [], + "execution_count": 8, + "id": "a2fafcc8", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:34:22.169098Z", + "iopub.status.busy": "2026-03-14T15:34:22.169019Z", + "iopub.status.idle": "2026-03-14T15:34:22.378149Z", + "shell.execute_reply": "2026-03-14T15:34:22.377711Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0.92, 'Toolbox-model spatial intensity comparison')" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 7: Visualize the firing rates as a function of the spatial covariates\n", "fig = _prepare_figure(\"mesh(x_new,y_new,lambda)\", figsize=(9.0, 6.5))\n", @@ -185,31 +295,58 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "eef1351f", - "metadata": {}, - "outputs": [], + "execution_count": 9, + "id": "64cdac30", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:34:22.379530Z", + "iopub.status.busy": "2026-03-14T15:34:22.379456Z", + "iopub.status.idle": "2026-03-14T15:34:22.388199Z", + "shell.execute_reply": "2026-03-14T15:34:22.387760Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "b - fitResults.b{2} = [ 4.23646893 -1.45765976 -3.22632654 -6.34196965 -4.18056197 -0.36152565]\n" + ] + } + ], "source": [ "# SECTION 8: Toolbox vs. Standard GLM comparison\n", - "standard_fit = fit_poisson_glm(np.column_stack([np.ones_like(xN), xN, yN, xN**2, yN**2, xN * yN]), spikes_binned, include_intercept=False)\n", - "coeff_diff = np.asarray(standard_fit.coefficients - fitResults.getCoeffs(2)[0], dtype=float)\n", - "fig = _prepare_figure(\"b-fitResults.b{2}\", figsize=(7.0, 4.5))\n", - "ax = fig.subplots(1, 1)\n", - "labels = [\"mu\", \"x\", \"y\", \"x^2\", \"y^2\", \"x*y\"]\n", - "ax.bar(np.arange(coeff_diff.size), coeff_diff, color=\"tab:blue\")\n", - "ax.axhline(0.0, color=\"0.3\", linestyle=\"--\", linewidth=1.0)\n", - "ax.set_xticks(np.arange(coeff_diff.size), labels, rotation=20)\n", - "ax.set_ylabel(\"standard minus toolbox\")\n", - "ax.set_title(\"Coefficient agreement between workflows\")\n", - "print({\"quadratic_coeff_diff_max_abs\": round(float(np.max(np.abs(coeff_diff))), 6)})\n" + "# Compare the nSTAT fit with a standalone glmfit using the same Quadratic covariates\n", + "# MATLAB: [b,dev,stats] = glmfit([xN yN xN.^2 yN.^2 xN.*yN], spikes_binned, 'poisson');\n", + "# b - fitResults.b{2} % should be close to zero\n", + "X_quad = np.column_stack([xN, yN, xN**2, yN**2, xN * yN])\n", + "glm_result = fit_poisson_glm(X_quad, spikes_binned)\n", + "b = np.concatenate([[glm_result.intercept], glm_result.coefficients])\n", + "b_diff = b - fitResults.getCoeffs(2)[0]\n", + "print(\"b - fitResults.b{2} =\", b_diff)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "04cd0c92", - "metadata": {}, - "outputs": [], + "execution_count": 10, + "id": "8782d383", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:34:22.389149Z", + "iopub.status.busy": "2026-03-14T15:34:22.389082Z", + "iopub.status.idle": "2026-03-14T15:39:05.300812Z", + "shell.execute_reply": "2026-03-14T15:39:05.300400Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'history_config_names': ['Baseline', 'Window1', 'Window2', 'Window3', 'Window4', 'Window5', 'Window6', 'Window7', 'Window8', 'Window9', 'Window10'], 'summary_fit_names': ['Baseline', 'Window1', 'Window2', 'Window3', 'Window4', 'Window5', 'Window6', 'Window7', 'Window8', 'Window9', 'Window10']}\n" + ] + } + ], "source": [ "# SECTION 9: Compute the history effect\n", "windowTimes = np.arange(0.0, 11.0) / sample_rate\n", @@ -229,7 +366,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 5, @@ -240,4 +386,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/ConfidenceIntervalOverview.ipynb b/notebooks/ConfidenceIntervalOverview.ipynb index ba0ac853..bc2ad67e 100644 --- a/notebooks/ConfidenceIntervalOverview.ipynb +++ b/notebooks/ConfidenceIntervalOverview.ipynb @@ -22,10 +22,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "e50c571b", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:39:16.390512Z", + "iopub.status.busy": "2026-03-14T15:39:16.390159Z", + "iopub.status.idle": "2026-03-14T15:39:18.763073Z", + "shell.execute_reply": "2026-03-14T15:39:18.762299Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from __future__ import annotations\n", "\n", @@ -54,10 +72,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "badf84eb", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:39:18.765705Z", + "iopub.status.busy": "2026-03-14T15:39:18.765445Z", + "iopub.status.idle": "2026-03-14T15:39:18.769078Z", + "shell.execute_reply": "2026-03-14T15:39:18.768482Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'time': [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0],\n", + " 'lower': [-0.15, 0.801, 0.438, -0.738, -1.101, -0.15],\n", + " 'upper': [0.15, 1.101, 0.738, -0.438, -0.801, 0.15],\n", + " 'color': 'tab:blue'}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "{\n", " \"time\": ci.time.tolist(),\n", @@ -79,10 +118,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "270611b9", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:39:18.770756Z", + "iopub.status.busy": "2026-03-14T15:39:18.770644Z", + "iopub.status.idle": "2026-03-14T15:39:18.773414Z", + "shell.execute_reply": "2026-03-14T15:39:18.772928Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError\n", + "bounds must have shape (n, 2)\n" + ] + } + ], "source": [ "try:\n", " ConfidenceInterval(time, np.ones((len(time), 3)))\n", @@ -111,8 +166,16 @@ "name": "python3" }, "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", "name": "python", - "version": "3.12" + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/ConfigCollExamples.ipynb b/notebooks/ConfigCollExamples.ipynb index 00ba6d08..c1f31483 100644 --- a/notebooks/ConfigCollExamples.ipynb +++ b/notebooks/ConfigCollExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "2c49c458", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:39:31.737054Z", + "iopub.status.busy": "2026-03-14T15:39:31.736776Z", + "iopub.status.idle": "2026-03-14T15:39:34.206271Z", + "shell.execute_reply": "2026-03-14T15:39:34.205172Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: ConfigCollExamples\n", @@ -38,7 +45,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/CovCollExamples.ipynb b/notebooks/CovCollExamples.ipynb index a4b77da7..08527231 100644 --- a/notebooks/CovCollExamples.ipynb +++ b/notebooks/CovCollExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "4bd1f199", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:39:45.988010Z", + "iopub.status.busy": "2026-03-14T15:39:45.987742Z", + "iopub.status.idle": "2026-03-14T15:39:48.153990Z", + "shell.execute_reply": "2026-03-14T15:39:48.153520Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: CovCollExamples\n", @@ -68,7 +75,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 2, diff --git a/notebooks/CovariateExamples.ipynb b/notebooks/CovariateExamples.ipynb index 9bdcd952..a2e18352 100644 --- a/notebooks/CovariateExamples.ipynb +++ b/notebooks/CovariateExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "e304053b", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:00.565371Z", + "iopub.status.busy": "2026-03-14T15:40:00.565075Z", + "iopub.status.idle": "2026-03-14T15:40:02.823504Z", + "shell.execute_reply": "2026-03-14T15:40:02.822724Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: CovariateExamples\n", @@ -36,9 +43,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "9be9e8f7", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:02.826781Z", + "iopub.status.busy": "2026-03-14T15:40:02.826517Z", + "iopub.status.idle": "2026-03-14T15:40:02.915172Z", + "shell.execute_reply": "2026-03-14T15:40:02.914725Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example 1: Using Covariates\n", @@ -66,7 +80,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 2, diff --git a/notebooks/DecodingExample.ipynb b/notebooks/DecodingExample.ipynb index 8b2e695c..7ec4a141 100644 --- a/notebooks/DecodingExample.ipynb +++ b/notebooks/DecodingExample.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "bb9c9790", - "metadata": {}, + "execution_count": 1, + "id": "44d88ec9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:13.804860Z", + "iopub.status.busy": "2026-03-14T15:40:13.804571Z", + "iopub.status.idle": "2026-03-14T15:40:16.103629Z", + "shell.execute_reply": "2026-03-14T15:40:16.103092Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: DecodingExample\n", @@ -82,10 +89,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c9032274", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "2f1ec431", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:16.105260Z", + "iopub.status.busy": "2026-03-14T15:40:16.105098Z", + "iopub.status.idle": "2026-03-14T15:40:16.443646Z", + "shell.execute_reply": "2026-03-14T15:40:16.443185Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'Hz')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 1: Generate the conditional Intensity Function\n", "plt.close(\"all\")\n", @@ -115,10 +140,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "282af22b", - "metadata": {}, - "outputs": [], + "execution_count": 3, + "id": "a7048402", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:16.445079Z", + "iopub.status.busy": "2026-03-14T15:40:16.444997Z", + "iopub.status.idle": "2026-03-14T15:40:36.287761Z", + "shell.execute_reply": "2026-03-14T15:40:36.287351Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Mean log-likelihood across neurons')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 2: Fit a model to the spikedata to obtain a model CIF\n", "stim = Covariate(time, x, \"Stimulus\", \"time\", \"s\", \"V\", [\"stim\"])\n", @@ -179,9 +222,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "49257a4c", - "metadata": {}, + "execution_count": 4, + "id": "df079e59", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:36.289103Z", + "iopub.status.busy": "2026-03-14T15:40:36.289003Z", + "iopub.status.idle": "2026-03-14T15:40:37.768356Z", + "shell.execute_reply": "2026-03-14T15:40:37.767817Z" + } + }, "outputs": [], "source": [ "# SECTION 3: Decode the stimulus from the fitted CIF\n", @@ -204,7 +254,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 5, diff --git a/notebooks/DecodingExampleWithHist.ipynb b/notebooks/DecodingExampleWithHist.ipynb index 32e9549d..0481daf4 100644 --- a/notebooks/DecodingExampleWithHist.ipynb +++ b/notebooks/DecodingExampleWithHist.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "0a4bc8bf", - "metadata": {}, + "execution_count": 1, + "id": "a847f096", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:44.311665Z", + "iopub.status.busy": "2026-03-14T15:40:44.311390Z", + "iopub.status.idle": "2026-03-14T15:40:46.493379Z", + "shell.execute_reply": "2026-03-14T15:40:46.492541Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: DecodingExampleWithHist\n", @@ -100,9 +107,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "7a31db6f", - "metadata": {}, + "execution_count": 2, + "id": "b9bfa418", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:40:46.496323Z", + "iopub.status.busy": "2026-03-14T15:40:46.496041Z", + "iopub.status.idle": "2026-03-14T15:43:34.976298Z", + "shell.execute_reply": "2026-03-14T15:43:34.975912Z" + } + }, "outputs": [], "source": [ "# SECTION 1: History-aware decoding workflow\n", @@ -163,7 +177,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 2, diff --git a/notebooks/EventsExamples.ipynb b/notebooks/EventsExamples.ipynb index c5bff681..445a211e 100644 --- a/notebooks/EventsExamples.ipynb +++ b/notebooks/EventsExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b27ad078", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:39.760409Z", + "iopub.status.busy": "2026-03-14T15:43:39.760306Z", + "iopub.status.idle": "2026-03-14T15:43:42.165621Z", + "shell.execute_reply": "2026-03-14T15:43:42.165139Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: EventsExamples\n", @@ -62,7 +69,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 3, diff --git a/notebooks/ExplicitStimulusWhiskerData.ipynb b/notebooks/ExplicitStimulusWhiskerData.ipynb index 4e4996d0..505528f6 100644 --- a/notebooks/ExplicitStimulusWhiskerData.ipynb +++ b/notebooks/ExplicitStimulusWhiskerData.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "5f00236d", - "metadata": {}, + "execution_count": 1, + "id": "e102e8bb", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:47.918144Z", + "iopub.status.busy": "2026-03-14T15:43:47.917909Z", + "iopub.status.idle": "2026-03-14T15:43:50.177531Z", + "shell.execute_reply": "2026-03-14T15:43:50.177075Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: ExplicitStimulusWhiskerData\n", @@ -82,10 +89,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ed25adb7", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "45734023", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:50.179569Z", + "iopub.status.busy": "2026-03-14T15:43:50.179410Z", + "iopub.status.idle": "2026-03-14T15:43:50.670983Z", + "shell.execute_reply": "2026-03-14T15:43:50.670428Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'n_samples': 51000, 'peak_lag_ms': 119.0, 'best_history_window_bins': 7}\n" + ] + } + ], "source": [ "# SECTION 0: EXPLICIT STIMULUS EXAMPLE - WHISKER STIMULATION/THALAMIC NEURON\n", "# This notebook follows the MATLAB helpfile workflow for explicit whisker-stimulation analysis.\n", @@ -105,10 +127,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c780cff1", - "metadata": {}, - "outputs": [], + "execution_count": 3, + "id": "0e41ab95", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:50.672354Z", + "iopub.status.busy": "2026-03-14T15:43:50.672253Z", + "iopub.status.idle": "2026-03-14T15:43:50.851243Z", + "shell.execute_reply": "2026-03-14T15:43:50.850755Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 1: Load the data\n", "fig = _prepare_figure(\"trial.plot\", figsize=(10.0, 6.0))\n", @@ -133,10 +173,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "6768c8ae", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "6c2191fc", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:50.852594Z", + "iopub.status.busy": "2026-03-14T15:43:50.852489Z", + "iopub.status.idle": "2026-03-14T15:43:50.969714Z", + "shell.execute_reply": "2026-03-14T15:43:50.969298Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 2: Fit a constant baseline\n", "fig = _prepare_figure(\"results.plotResults\", figsize=(6.0, 5.5))\n", @@ -148,10 +206,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e2145f7b", - "metadata": {}, - "outputs": [], + "execution_count": 5, + "id": "da4b0be4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:50.971192Z", + "iopub.status.busy": "2026-03-14T15:43:50.971093Z", + "iopub.status.idle": "2026-03-14T15:43:51.033897Z", + "shell.execute_reply": "2026-03-14T15:43:51.033306Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'cross-covariance')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 3: Find Stimulus Lag\n", "fig = _prepare_figure(\"results.Residual.xcov(stim).windowedSignal([0,1]).plot\", figsize=(8.5, 4.5))\n", @@ -169,10 +245,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ae00cde0", - "metadata": {}, - "outputs": [], + "execution_count": 6, + "id": "334952df", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:51.035271Z", + "iopub.status.busy": "2026-03-14T15:43:51.035160Z", + "iopub.status.idle": "2026-03-14T15:43:51.164920Z", + "shell.execute_reply": "2026-03-14T15:43:51.164559Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: Compare constant rate model with model including stimulus effect\n", "fig = _prepare_figure(\"results.plotResults\", figsize=(8.5, 4.5))\n", @@ -197,10 +291,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "8a22e891", - "metadata": {}, - "outputs": [], + "execution_count": 7, + "id": "eb6dc162", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:51.166454Z", + "iopub.status.busy": "2026-03-14T15:43:51.166353Z", + "iopub.status.idle": "2026-03-14T15:43:51.332855Z", + "shell.execute_reply": "2026-03-14T15:43:51.332378Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'ΔBIC relative to first history model')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: History Effect\n", "fig = _prepare_figure(\"Summary.plotSummary\", figsize=(9.0, 7.0))\n", @@ -224,14 +336,21 @@ "ax.axvline(history_windows[best_history_idx], color=\"tab:red\", linestyle=\"--\", linewidth=1.0)\n", "ax.set_title(\"BIC improvement across history-window choices\")\n", "ax.set_xlabel(\"history window count\")\n", - "ax.set_ylabel(\"ΔBIC relative to first history model\")\n" + "ax.set_ylabel(\"ΔBIC relative to first history model\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "5b152fd9", - "metadata": {}, + "execution_count": 8, + "id": "09de73d5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:51.334188Z", + "iopub.status.busy": "2026-03-14T15:43:51.334105Z", + "iopub.status.idle": "2026-03-14T15:43:51.505289Z", + "shell.execute_reply": "2026-03-14T15:43:51.504798Z" + } + }, "outputs": [], "source": [ "# SECTION 6: Compare Baseline, Baseline+Stimulus Model, Baseline+History+Stimulus\n", @@ -279,7 +398,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 9, @@ -290,4 +418,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/FitResSummaryExamples.ipynb b/notebooks/FitResSummaryExamples.ipynb index 79b17dd3..bddd8e21 100644 --- a/notebooks/FitResSummaryExamples.ipynb +++ b/notebooks/FitResSummaryExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "4b34dc65", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:43:59.849416Z", + "iopub.status.busy": "2026-03-14T15:43:59.849007Z", + "iopub.status.idle": "2026-03-14T15:44:02.102892Z", + "shell.execute_reply": "2026-03-14T15:44:02.102410Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: FitResSummaryExamples\n", @@ -54,7 +61,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/FitResultExamples.ipynb b/notebooks/FitResultExamples.ipynb index 03e97687..74a1822e 100644 --- a/notebooks/FitResultExamples.ipynb +++ b/notebooks/FitResultExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "0e277f93", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:05.938150Z", + "iopub.status.busy": "2026-03-14T15:44:05.937857Z", + "iopub.status.idle": "2026-03-14T15:44:08.249285Z", + "shell.execute_reply": "2026-03-14T15:44:08.248213Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: FitResultExamples\n", @@ -54,7 +61,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/FitResultReference.ipynb b/notebooks/FitResultReference.ipynb index c3992b4b..0806a20d 100644 --- a/notebooks/FitResultReference.ipynb +++ b/notebooks/FitResultReference.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "70940850", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:13.523805Z", + "iopub.status.busy": "2026-03-14T15:44:13.523686Z", + "iopub.status.idle": "2026-03-14T15:44:15.634065Z", + "shell.execute_reply": "2026-03-14T15:44:15.633442Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: FitResultReference\n", @@ -51,9 +58,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "8ae1a337", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:15.636690Z", + "iopub.status.busy": "2026-03-14T15:44:15.636452Z", + "iopub.status.idle": "2026-03-14T15:44:15.640678Z", + "shell.execute_reply": "2026-03-14T15:44:15.640086Z" + } + }, "outputs": [], "source": [ "# SECTION 1: FitResult Reference\n", @@ -75,7 +89,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/HippocampalPlaceCellExample.ipynb b/notebooks/HippocampalPlaceCellExample.ipynb index b3ad9d87..947f4962 100644 --- a/notebooks/HippocampalPlaceCellExample.ipynb +++ b/notebooks/HippocampalPlaceCellExample.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e0302c60", - "metadata": {}, + "execution_count": 1, + "id": "15ef53db", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:19.977910Z", + "iopub.status.busy": "2026-03-14T15:44:19.977661Z", + "iopub.status.idle": "2026-03-14T15:44:22.250484Z", + "shell.execute_reply": "2026-03-14T15:44:22.250074Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: HippocampalPlaceCellExample\n", @@ -84,10 +91,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "270e5d9d", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "30094bfc", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:22.251983Z", + "iopub.status.busy": "2026-03-14T15:44:22.251849Z", + "iopub.status.idle": "2026-03-14T15:44:22.402307Z", + "shell.execute_reply": "2026-03-14T15:44:22.401836Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_cells_fit': 8, 'mean_delta_aic': 87.651, 'mean_delta_bic': 44.358}\n" + ] + } + ], "source": [ "# SECTION 0: HIPPOCAMPAL PLACE CELL - RECEPTIVE FIELD ESTIMATION\n", "# This notebook mirrors the MATLAB place-cell helpfile using the dataset-backed Python workflow.\n", @@ -104,9 +126,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "8079dbfd", - "metadata": {}, + "execution_count": 3, + "id": "33671840", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:22.403587Z", + "iopub.status.busy": "2026-03-14T15:44:22.403505Z", + "iopub.status.idle": "2026-03-14T15:44:22.420874Z", + "shell.execute_reply": "2026-03-14T15:44:22.420483Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example Data\n", @@ -124,10 +153,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "cd6e5704", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "081e6179", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:22.422106Z", + "iopub.status.busy": "2026-03-14T15:44:22.422032Z", + "iopub.status.idle": "2026-03-14T15:44:22.570126Z", + "shell.execute_reply": "2026-03-14T15:44:22.569661Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Animal 1 BIC')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 2: Analyze All Cells\n", "fig = _prepare_figure(\"Summary.plotSummary\", figsize=(7.5, 4.5))\n", @@ -151,10 +198,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "2487ae0f", - "metadata": {}, - "outputs": [], + "execution_count": 5, + "id": "aa269c6b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:22.571509Z", + "iopub.status.busy": "2026-03-14T15:44:22.571408Z", + "iopub.status.idle": "2026-03-14T15:44:22.648867Z", + "shell.execute_reply": "2026-03-14T15:44:22.648424Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Animal 2 BIC')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 3: View Summary Statistics\n", "fig = _prepare_figure(\"Summary.plotSummary\", figsize=(7.5, 4.5))\n", @@ -178,10 +243,26 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "f92a805b", - "metadata": {}, - "outputs": [], + "execution_count": 6, + "id": "26aafec5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:22.650439Z", + "iopub.status.busy": "2026-03-14T15:44:22.650324Z", + "iopub.status.idle": "2026-03-14T15:44:23.601656Z", + "shell.execute_reply": "2026-03-14T15:44:23.601164Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", + " self._active_fig.tight_layout()\n" + ] + } + ], "source": [ "# SECTION 4: Visualize the results\n", "fig = _prepare_figure(\"h4=figure(4)\", figsize=(8.5, 8.0))\n", @@ -226,40 +307,34 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", "\n", - "fig = _prepare_figure(\"figure(10)\", figsize=(9.0, 4.5))\n", + "# figure(9) overlay — matches MATLAB hold-on composite (published as _10.png)\n", + "fig = _prepare_figure(\"figure(9) overlay\", figsize=(10.0, 5.0))\n", "axs = fig.subplots(1, 2)\n", - "axs[0].hist(np.concatenate([payload[\"animal1\"][\"delta_aic\"], payload[\"animal2\"][\"delta_aic\"]]), bins=8, color=\"tab:purple\", alpha=0.8)\n", - "axs[0].axvline(0.0, color=\"0.2\", linewidth=1.0)\n", - "axs[0].set_title(\"Distribution of ΔAIC\")\n", - "axs[1].hist(np.concatenate([payload[\"animal1\"][\"delta_bic\"], payload[\"animal2\"][\"delta_bic\"]]), bins=8, color=\"tab:green\", alpha=0.8)\n", - "axs[1].axvline(0.0, color=\"0.2\", linewidth=1.0)\n", - "axs[1].set_title(\"Distribution of ΔBIC\")\n", + "ext = [float(np.min(mesh[\"grid_x\"])), float(np.max(mesh[\"grid_x\"])), float(np.min(mesh[\"grid_y\"])), float(np.max(mesh[\"grid_y\"]))]\n", + "for ax, field, label in zip(axs, [mesh[\"gaussian_field\"], mesh[\"zernike_field\"]], [\"Gaussian\", \"Zernike\"]):\n", + " ax.imshow(field, origin=\"lower\", extent=ext, aspect=\"equal\", cmap=\"viridis\")\n", + " ax.plot(mesh[\"x_pos\"], mesh[\"y_pos\"], color=\"white\", linewidth=0.5, alpha=0.35)\n", + " ax.scatter(spike_x, spike_y, s=8, color=\"tab:red\", alpha=0.7)\n", + " ax.set_title(f\"{label} - Cell {int(mesh['cell_index']) + 1}\")\n", + " ax.set_xlabel(\"x\")\n", + " ax.set_ylabel(\"y\")\n", "\n", - "fig = _prepare_figure(\"figure(11)\", figsize=(6.5, 4.5))\n", - "ax = fig.subplots(1, 1)\n", - "ax.axis(\"off\")\n", - "ax.text(\n", - " 0.0,\n", - " 0.95,\n", - " \"\\n\".join(\n", - " [\n", - " f\"Cells analyzed: {int(summary['num_cells_fit'])}\",\n", - " f\"Mean Gaussian-Zernike ΔAIC: {summary['mean_delta_aic_gaussian_minus_zernike']:.2f}\",\n", - " f\"Mean Gaussian-Zernike ΔBIC: {summary['mean_delta_bic_gaussian_minus_zernike']:.2f}\",\n", - " \"Negative values favor the Zernike model.\",\n", - " ]\n", - " ),\n", - " va=\"top\",\n", - " family=\"monospace\",\n", - " fontsize=10,\n", - ")\n", - "__tracker.finalize()\n" + "__tracker.finalize()" ] } ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 11, @@ -270,4 +345,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/HistoryExamples.ipynb b/notebooks/HistoryExamples.ipynb index 173a3ff4..f5e7d240 100644 --- a/notebooks/HistoryExamples.ipynb +++ b/notebooks/HistoryExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "a336fb0f", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:28.059078Z", + "iopub.status.busy": "2026-03-14T15:44:28.058900Z", + "iopub.status.idle": "2026-03-14T15:44:30.377134Z", + "shell.execute_reply": "2026-03-14T15:44:30.376740Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: HistoryExamples\n", @@ -54,9 +61,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "85845f70", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:30.378821Z", + "iopub.status.busy": "2026-03-14T15:44:30.378685Z", + "iopub.status.idle": "2026-03-14T15:44:30.429376Z", + "shell.execute_reply": "2026-03-14T15:44:30.428923Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example 1: History covariates for one neural spike train\n", @@ -81,9 +95,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "393bfd7d", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:30.430585Z", + "iopub.status.busy": "2026-03-14T15:44:30.430512Z", + "iopub.status.idle": "2026-03-14T15:44:30.663186Z", + "shell.execute_reply": "2026-03-14T15:44:30.662781Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Example 2: History covariates for a collection of Neural Spikes (nstColl)\n", @@ -114,7 +135,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 3, diff --git a/notebooks/HybridFilterExample.ipynb b/notebooks/HybridFilterExample.ipynb index 2d84d178..6284b552 100644 --- a/notebooks/HybridFilterExample.ipynb +++ b/notebooks/HybridFilterExample.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "90518773", - "metadata": {}, + "execution_count": 1, + "id": "d22dd216", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:38.677868Z", + "iopub.status.busy": "2026-03-14T15:44:38.677584Z", + "iopub.status.idle": "2026-03-14T15:44:40.831630Z", + "shell.execute_reply": "2026-03-14T15:44:40.830774Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: HybridFilterExample\n", @@ -62,10 +69,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "1bccc86c", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "cacac4c3", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:40.834345Z", + "iopub.status.busy": "2026-03-14T15:44:40.834116Z", + "iopub.status.idle": "2026-03-14T15:44:41.334276Z", + "shell.execute_reply": "2026-03-14T15:44:41.333835Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_samples': 3000, 'num_cells': 24, 'state_accuracy': 0.899}\n" + ] + } + ], "source": [ "# SECTION 0: Hybrid Point Process Filter Example\n", "# This notebook mirrors the MATLAB hybrid-filter helpfile with executable figures.\n", @@ -86,9 +108,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "5f1f9aee", - "metadata": {}, + "execution_count": 3, + "id": "b031dd85", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:41.335603Z", + "iopub.status.busy": "2026-03-14T15:44:41.335525Z", + "iopub.status.idle": "2026-03-14T15:44:41.337214Z", + "shell.execute_reply": "2026-03-14T15:44:41.336812Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Problem Statement\n", @@ -97,9 +126,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "93db6f6c", - "metadata": {}, + "execution_count": 4, + "id": "fd11f1d4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:41.338199Z", + "iopub.status.busy": "2026-03-14T15:44:41.338134Z", + "iopub.status.idle": "2026-03-14T15:44:41.339669Z", + "shell.execute_reply": "2026-03-14T15:44:41.339289Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Hybrid state-space setup\n", @@ -108,10 +144,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "215fd0f4", - "metadata": {}, - "outputs": [], + "execution_count": 5, + "id": "fd690f04", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:41.340739Z", + "iopub.status.busy": "2026-03-14T15:44:41.340668Z", + "iopub.status.idle": "2026-03-14T15:44:41.411997Z", + "shell.execute_reply": "2026-03-14T15:44:41.411499Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.0, 0.95, 'Cells: 24\\nState accuracy: 0.899\\nDecode RMSE X: 0.216\\nDecode RMSE Y: 0.172')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 3: Generated Simulated Arm Reach\n", "fig = _prepare_figure(\"fig1=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 ...\", figsize=(10.0, 9.0))\n", @@ -160,9 +214,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c7531b74", - "metadata": {}, + "execution_count": 6, + "id": "f9e4eb9d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:41.413285Z", + "iopub.status.busy": "2026-03-14T15:44:41.413201Z", + "iopub.status.idle": "2026-03-14T15:44:41.414819Z", + "shell.execute_reply": "2026-03-14T15:44:41.414478Z" + } + }, "outputs": [], "source": [ "# SECTION 4: Simulate Neural Firing\n", @@ -171,9 +232,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "90456226", - "metadata": {}, + "execution_count": 7, + "id": "57220fb5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:41.415928Z", + "iopub.status.busy": "2026-03-14T15:44:41.415851Z", + "iopub.status.idle": "2026-03-14T15:44:42.019199Z", + "shell.execute_reply": "2026-03-14T15:44:42.018661Z" + } + }, "outputs": [], "source": [ "# SECTION 5: Run the hybrid filter\n", @@ -237,7 +305,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 3, diff --git a/notebooks/NetworkTutorial.ipynb b/notebooks/NetworkTutorial.ipynb index 5e0df873..129dfb16 100644 --- a/notebooks/NetworkTutorial.ipynb +++ b/notebooks/NetworkTutorial.ipynb @@ -14,10 +14,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "900fa7c2", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:46.577062Z", + "iopub.status.busy": "2026-03-14T15:44:46.576958Z", + "iopub.status.idle": "2026-03-14T15:44:48.759621Z", + "shell.execute_reply": "2026-03-14T15:44:48.759137Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "<>:100: SyntaxWarning: invalid escape sequence '\\c'\n", + "<>:101: SyntaxWarning: invalid escape sequence '\\c'\n", + "<>:100: SyntaxWarning: invalid escape sequence '\\c'\n", + "<>:101: SyntaxWarning: invalid escape sequence '\\c'\n", + "/var/folders/tg/z6dfb8b13wg_h4f3v8whzpgh0000gn/T/ipykernel_67250/2870471293.py:100: SyntaxWarning: invalid escape sequence '\\c'\n", + " ax.text(0.25, 0.14, \"$S_1=+1 \\cdot u_{stim}$\", ha=\"center\", fontsize=10)\n", + "/var/folders/tg/z6dfb8b13wg_h4f3v8whzpgh0000gn/T/ipykernel_67250/2870471293.py:101: SyntaxWarning: invalid escape sequence '\\c'\n", + " ax.text(0.75, 0.14, \"$S_2=-1 \\cdot u_{stim}$\", ha=\"center\", fontsize=10)\n" + ] + } + ], "source": [ "# nSTAT-python notebook example: NetworkTutorial\n", "from pathlib import Path\n", @@ -159,9 +181,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "bf0f3cb7", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:48.761330Z", + "iopub.status.busy": "2026-03-14T15:44:48.761169Z", + "iopub.status.idle": "2026-03-14T15:44:48.763033Z", + "shell.execute_reply": "2026-03-14T15:44:48.762617Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Point Process Network Simulation\n", @@ -171,9 +200,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "e8398ef5", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:48.764175Z", + "iopub.status.busy": "2026-03-14T15:44:48.764047Z", + "iopub.status.idle": "2026-03-14T15:44:48.784641Z", + "shell.execute_reply": "2026-03-14T15:44:48.784011Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Published network diagram\n", @@ -184,9 +220,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "a6428314", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:48.786267Z", + "iopub.status.busy": "2026-03-14T15:44:48.786161Z", + "iopub.status.idle": "2026-03-14T15:44:48.941718Z", + "shell.execute_reply": "2026-03-14T15:44:48.941160Z" + } + }, "outputs": [], "source": [ "# SECTION 3: Published block diagram\n", @@ -197,10 +240,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "fe878a9f", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:48.943289Z", + "iopub.status.busy": "2026-03-14T15:44:48.943174Z", + "iopub.status.idle": "2026-03-14T15:44:49.005783Z", + "shell.execute_reply": "2026-03-14T15:44:49.005428Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: Conditional intensity equation\n", "fig = _figure(\"lambda_i * Delta = logistic(mu_i + H*DeltaN_i[n] + S*u_stim[n] + E*DeltaN_k[n])\", figsize=(10.0, 3.0))\n", @@ -216,9 +277,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "303ba752", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:49.006956Z", + "iopub.status.busy": "2026-03-14T15:44:49.006886Z", + "iopub.status.idle": "2026-03-14T15:44:49.008397Z", + "shell.execute_reply": "2026-03-14T15:44:49.007986Z" + } + }, "outputs": [], "source": [ "# SECTION 5: Logistic nonlinearity\n", @@ -227,9 +295,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "ed73bb76", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:49.009318Z", + "iopub.status.busy": "2026-03-14T15:44:49.009251Z", + "iopub.status.idle": "2026-03-14T15:44:49.010726Z", + "shell.execute_reply": "2026-03-14T15:44:49.010341Z" + } + }, "outputs": [], "source": [ "# SECTION 6: Convolution operator note\n", @@ -238,9 +313,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "39438bb1", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:44:49.011695Z", + "iopub.status.busy": "2026-03-14T15:44:49.011609Z", + "iopub.status.idle": "2026-03-14T15:45:27.439907Z", + "shell.execute_reply": "2026-03-14T15:45:27.439393Z" + } + }, "outputs": [], "source": [ "# SECTION 7: 2 Neuron Network\n", @@ -262,10 +344,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "fa0f59ac", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.441812Z", + "iopub.status.busy": "2026-03-14T15:45:27.441693Z", + "iopub.status.idle": "2026-03-14T15:45:27.443782Z", + "shell.execute_reply": "2026-03-14T15:45:27.443217Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'mu1': -3.0, 'mu2': -3.0, 'sample_rate_hz': 1000.0}\n" + ] + } + ], "source": [ "# SECTION 8: Baseline firing rate of the neurons being modeled\n", "print({\"mu1\": float(baseline_mu[0]), \"mu2\": float(baseline_mu[1]), \"sample_rate_hz\": sampleRate})\n" @@ -273,9 +370,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "71852dc5", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.445064Z", + "iopub.status.busy": "2026-03-14T15:45:27.444975Z", + "iopub.status.idle": "2026-03-14T15:45:27.446662Z", + "shell.execute_reply": "2026-03-14T15:45:27.446262Z" + } + }, "outputs": [], "source": [ "# SECTION 9: History Effect\n", @@ -284,10 +388,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "d9bb524a", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.447744Z", + "iopub.status.busy": "2026-03-14T15:45:27.447646Z", + "iopub.status.idle": "2026-03-14T15:45:27.483376Z", + "shell.execute_reply": "2026-03-14T15:45:27.483012Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[Text(1.0, 0, '1'), Text(2.0, 0, '2'), Text(3.0, 0, '3')]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 10: History kernel\n", "fig = _figure(\"1*h[n]=-4*DeltaN[n-1]-2*DeltaN[n-2]-1*DeltaN[n-3]\", figsize=(8.0, 4.5))\n", @@ -298,9 +420,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "8dd40443", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.484704Z", + "iopub.status.busy": "2026-03-14T15:45:27.484627Z", + "iopub.status.idle": "2026-03-14T15:45:27.486244Z", + "shell.execute_reply": "2026-03-14T15:45:27.485828Z" + } + }, "outputs": [], "source": [ "# SECTION 11: Stimulus Effect\n", @@ -309,9 +438,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "cbee27bd", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.487342Z", + "iopub.status.busy": "2026-03-14T15:45:27.487275Z", + "iopub.status.idle": "2026-03-14T15:45:27.530838Z", + "shell.execute_reply": "2026-03-14T15:45:27.530460Z" + } + }, "outputs": [], "source": [ "# SECTION 12: Stimulus filter for neuron 1\n", @@ -322,9 +458,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "a4faf7c3", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.531955Z", + "iopub.status.busy": "2026-03-14T15:45:27.531885Z", + "iopub.status.idle": "2026-03-14T15:45:27.570206Z", + "shell.execute_reply": "2026-03-14T15:45:27.569726Z" + } + }, "outputs": [], "source": [ "# SECTION 13: Stimulus filter for neuron 2\n", @@ -335,9 +478,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "5c89e3bf", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.571600Z", + "iopub.status.busy": "2026-03-14T15:45:27.571519Z", + "iopub.status.idle": "2026-03-14T15:45:27.573153Z", + "shell.execute_reply": "2026-03-14T15:45:27.572759Z" + } + }, "outputs": [], "source": [ "# SECTION 14: Ensemble Effect\n", @@ -346,9 +496,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "e63fe4b7", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.574264Z", + "iopub.status.busy": "2026-03-14T15:45:27.574200Z", + "iopub.status.idle": "2026-03-14T15:45:27.611584Z", + "shell.execute_reply": "2026-03-14T15:45:27.611183Z" + } + }, "outputs": [], "source": [ "# SECTION 15: Ensemble filter for neuron 1\n", @@ -359,9 +516,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "585ef59c", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.612681Z", + "iopub.status.busy": "2026-03-14T15:45:27.612608Z", + "iopub.status.idle": "2026-03-14T15:45:27.650194Z", + "shell.execute_reply": "2026-03-14T15:45:27.649801Z" + } + }, "outputs": [], "source": [ "# SECTION 16: Ensemble filter for neuron 2\n", @@ -372,9 +536,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "c491c656", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.651242Z", + "iopub.status.busy": "2026-03-14T15:45:27.651170Z", + "iopub.status.idle": "2026-03-14T15:45:27.653402Z", + "shell.execute_reply": "2026-03-14T15:45:27.653002Z" + } + }, "outputs": [], "source": [ "# SECTION 17: Stimulus\n", @@ -385,10 +556,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "68e45b9a", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:27.654399Z", + "iopub.status.busy": "2026-03-14T15:45:27.654335Z", + "iopub.status.idle": "2026-03-14T15:45:29.277280Z", + "shell.execute_reply": "2026-03-14T15:45:29.276843Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 18: Simulate the Network\n", "fitType = \"binomial\"\n", @@ -419,9 +608,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "31945e6f", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:29.278628Z", + "iopub.status.busy": "2026-03-14T15:45:29.278534Z", + "iopub.status.idle": "2026-03-14T15:45:29.280512Z", + "shell.execute_reply": "2026-03-14T15:45:29.280201Z" + } + }, "outputs": [], "source": [ "# SECTION 19: GLM Model Fitting Setup\n", @@ -433,10 +629,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "286a3cc1", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:29.281694Z", + "iopub.status.busy": "2026-03-14T15:45:29.281610Z", + "iopub.status.idle": "2026-03-14T15:45:44.708227Z", + "shell.execute_reply": "2026-03-14T15:45:44.707807Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 20: GLM Model Fitting and Results\n", "results = Analysis.RunAnalysisForAllNeurons(trial, cfgColl, 0, Algorithm)\n", @@ -470,10 +684,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "f31f67ab", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:44.709848Z", + "iopub.status.busy": "2026-03-14T15:45:44.709749Z", + "iopub.status.idle": "2026-03-14T15:45:44.787739Z", + "shell.execute_reply": "2026-03-14T15:45:44.787291Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'algorithm': 'BNLRCG', 'spike_counts': [2546, 2327], 'estimated_network': [[0.0, 0.0], [0.0, 0.0]]}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", + " self._active_fig.tight_layout()\n" + ] + } + ], "source": [ "# SECTION 21: Neighbor-selection note\n", "# By default all neurons are considered potential neighbors. To restrict candidate neighbors, call trial.setNeighbors(neighborArray) using the MATLAB-style convention described in the source helpfile.\n", @@ -490,7 +727,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 14, @@ -501,4 +747,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/PPSimExample.ipynb b/notebooks/PPSimExample.ipynb index 5d7c3168..cd6812af 100644 --- a/notebooks/PPSimExample.ipynb +++ b/notebooks/PPSimExample.ipynb @@ -14,10 +14,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "a6759a0c", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:45:52.830232Z", + "iopub.status.busy": "2026-03-14T15:45:52.830019Z", + "iopub.status.idle": "2026-03-14T15:46:19.101796Z", + "shell.execute_reply": "2026-03-14T15:46:19.101370Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Warning: The specified simulation stop time (50.0) is not an integer multiple of the fixed step size (0.00099999999999988987). Changing the stop time to (49.999999999994493). \n", + "Suggested Actions:\n", + " • Specify an integer multiple of the fixed step size for the stop time of the simulation. - Fix\n", + " • Set Automatic solver parameter selection diagnostic to 'none'. - Apply\n", + "\n", + "Warning: The specified simulation stop time (50.0) is not an integer multiple of the fixed step size (0.00099999999999988987). Changing the stop time to (49.999999999994493). \n", + "Suggested Actions:\n", + " • Specify an integer multiple of the fixed step size for the stop time of the simulation. - Fix\n", + " • Set Automatic solver parameter selection diagnostic to 'none'. - Apply\n", + "\n", + "Warning: The specified simulation stop time (50.0) is not an integer multiple of the fixed step size (0.00099999999999988987). Changing the stop time to (49.999999999994493). \n", + "Suggested Actions:\n", + " • Specify an integer multiple of the fixed step size for the stop time of the simulation. - Fix\n", + " • Set Automatic solver parameter selection diagnostic to 'none'. - Apply\n", + "\n", + "Warning: The specified simulation stop time (50.0) is not an integer multiple of the fixed step size (0.00099999999999988987). Changing the stop time to (49.999999999994493). \n", + "Suggested Actions:\n", + " • Specify an integer multiple of the fixed step size for the stop time of the simulation. - Fix\n", + " • Set Automatic solver parameter selection diagnostic to 'none'. - Apply\n", + "\n", + "Warning: The specified simulation stop time (50.0) is not an integer multiple of the fixed step size (0.00099999999999988987). Changing the stop time to (49.999999999994493). \n", + "Suggested Actions:\n", + " • Specify an integer multiple of the fixed step size for the stop time of the simulation. - Fix\n", + " • Set Automatic solver parameter selection diagnostic to 'none'. - Apply\n", + "\n", + "{'duration_s': 50.0, 'num_realizations': 5, 'mean_rate_hz': 48.949}\n" + ] + } + ], "source": [ "# nSTAT-python notebook example: PPSimExample\n", "from pathlib import Path\n", @@ -68,9 +108,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "fb22ed56", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.103416Z", + "iopub.status.busy": "2026-03-14T15:46:19.103250Z", + "iopub.status.idle": "2026-03-14T15:46:19.105166Z", + "shell.execute_reply": "2026-03-14T15:46:19.104734Z" + } + }, "outputs": [], "source": [ "# SECTION 1: General Point Process Simulation\n", @@ -79,10 +126,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "aefbf353", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.106194Z", + "iopub.status.busy": "2026-03-14T15:46:19.106124Z", + "iopub.status.idle": "2026-03-14T15:46:19.108147Z", + "shell.execute_reply": "2026-03-14T15:46:19.107674Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using native Python CIF.simulateCIF to mirror the MATLAB recursive-CIF workflow.\n" + ] + } + ], "source": [ "# SECTION 2: Point Process Sample Path Generation\n", "print(\"Using native Python CIF.simulateCIF to mirror the MATLAB recursive-CIF workflow.\")" @@ -90,10 +152,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "ae867985", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.109355Z", + "iopub.status.busy": "2026-03-14T15:46:19.109279Z", + "iopub.status.idle": "2026-03-14T15:46:19.111392Z", + "shell.execute_reply": "2026-03-14T15:46:19.110872Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'history_windows_s': [0.0, 0.001, 0.002, 0.003]}\n" + ] + } + ], "source": [ "# SECTION 3: History Effect\n", "selfHist = [0.0, 0.001, 0.002, 0.003]\n", @@ -102,10 +179,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "9076f004", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.112645Z", + "iopub.status.busy": "2026-03-14T15:46:19.112572Z", + "iopub.status.idle": "2026-03-14T15:46:19.114390Z", + "shell.execute_reply": "2026-03-14T15:46:19.114053Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'stimulus_frequency_hz': 1.0, 'stimulus_amplitude': 1.0}\n" + ] + } + ], "source": [ "# SECTION 4: Stimulus Effect\n", "print({\"stimulus_frequency_hz\": 1.0, \"stimulus_amplitude\": 1.0})" @@ -113,10 +205,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "fa8120b8", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.115743Z", + "iopub.status.busy": "2026-03-14T15:46:19.115661Z", + "iopub.status.idle": "2026-03-14T15:46:19.117529Z", + "shell.execute_reply": "2026-03-14T15:46:19.117158Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'ensemble_effect': 0.0}\n" + ] + } + ], "source": [ "# SECTION 5: Ensemble Effect\n", "print({\"ensemble_effect\": 0.0})" @@ -124,10 +231,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "c58f3108", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:19.118530Z", + "iopub.status.busy": "2026-03-14T15:46:19.118460Z", + "iopub.status.idle": "2026-03-14T15:46:20.534134Z", + "shell.execute_reply": "2026-03-14T15:46:20.533685Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.0, 10.0)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 6: Generate sample paths\n", "fig = _figure(\"figure; subplot(2,1,1); sC.plot; subplot(2,1,2); stim.plot\", figsize=(10.0, 5.5))\n", @@ -140,10 +265,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "a7b37585", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:20.535378Z", + "iopub.status.busy": "2026-03-14T15:46:20.535294Z", + "iopub.status.idle": "2026-03-14T15:46:21.348612Z", + "shell.execute_reply": "2026-03-14T15:46:21.348131Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.0, 10.0)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 7: Inspect the simulated CIF\n", "fig = _figure(\"figure; lambda.plot\", figsize=(10.0, 4.0))\n", @@ -154,9 +297,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "bac3e6f1", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:21.349911Z", + "iopub.status.busy": "2026-03-14T15:46:21.349825Z", + "iopub.status.idle": "2026-03-14T15:46:21.351965Z", + "shell.execute_reply": "2026-03-14T15:46:21.351553Z" + } + }, "outputs": [], "source": [ "# SECTION 8: GLM Model Fitting Setup\n", @@ -170,10 +320,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "278b16e6", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:21.352971Z", + "iopub.status.busy": "2026-03-14T15:46:21.352901Z", + "iopub.status.idle": "2026-03-14T15:46:21.587071Z", + "shell.execute_reply": "2026-03-14T15:46:21.586523Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'algorithm': 'BNLRCG', 'binary_representation': True}\n" + ] + } + ], "source": [ "# SECTION 9: Choose the MATLAB-style fitting algorithm\n", "Algorithm = \"BNLRCG\"\n", @@ -182,9 +347,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "1c9f83d1", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:46:21.588266Z", + "iopub.status.busy": "2026-03-14T15:46:21.588181Z", + "iopub.status.idle": "2026-03-14T15:47:07.981522Z", + "shell.execute_reply": "2026-03-14T15:47:07.981031Z" + } + }, "outputs": [], "source": [ "# SECTION 10: GLM Model Fitting and Results\n", @@ -193,10 +365,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "c939f67c", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:07.983092Z", + "iopub.status.busy": "2026-03-14T15:47:07.983003Z", + "iopub.status.idle": "2026-03-14T15:47:08.683659Z", + "shell.execute_reply": "2026-03-14T15:47:08.683134Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 11: Results for sample neuron\n", "fig = _figure(\"results{1}.plotResults\", figsize=(11.0, 8.0))\n", @@ -205,10 +395,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "21f39091", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:08.685125Z", + "iopub.status.busy": "2026-03-14T15:47:08.685015Z", + "iopub.status.idle": "2026-03-14T15:47:09.581813Z", + "shell.execute_reply": "2026-03-14T15:47:09.581078Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 12: Baseline-only diagnostic view\n", "fig = _figure(\"results{1}.plotResults baseline\", figsize=(11.0, 8.0))\n", @@ -217,10 +425,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "6faa0468", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:09.583300Z", + "iopub.status.busy": "2026-03-14T15:47:09.583169Z", + "iopub.status.idle": "2026-03-14T15:47:10.452684Z", + "shell.execute_reply": "2026-03-14T15:47:10.452054Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 13: Stimulus model diagnostic view\n", "fig = _figure(\"results{2}.plotResults stim\", figsize=(11.0, 8.0))\n", @@ -229,10 +455,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "27d1e5af", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:10.453876Z", + "iopub.status.busy": "2026-03-14T15:47:10.453788Z", + "iopub.status.idle": "2026-03-14T15:47:11.336477Z", + "shell.execute_reply": "2026-03-14T15:47:11.335875Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 14: Stimulus-plus-history diagnostic view\n", "fig = _figure(\"results{3}.plotResults hist\", figsize=(11.0, 8.0))\n", @@ -241,10 +485,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "bd5e5ca2", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:11.337949Z", + "iopub.status.busy": "2026-03-14T15:47:11.337834Z", + "iopub.status.idle": "2026-03-14T15:47:11.624213Z", + "shell.execute_reply": "2026-03-14T15:47:11.623854Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.0, 10.0)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 15: Compare fitted firing rates\n", "fig = _figure(\"results.lambda.plot\", figsize=(9.5, 4.5))\n", @@ -255,10 +517,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "9d323187", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:11.625934Z", + "iopub.status.busy": "2026-03-14T15:47:11.625837Z", + "iopub.status.idle": "2026-03-14T15:47:11.740089Z", + "shell.execute_reply": "2026-03-14T15:47:11.739726Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'fit_names': ['Baseline', 'Stim', 'Stim+Hist'], 'mean_AIC': [14768.013, 14013.036, 13415.194]}\n" + ] + } + ], "source": [ "# SECTION 16: Results across all sample paths\n", "summary = FitResSummary(results)\n", @@ -269,9 +546,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "0c7da4fb", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:11.741550Z", + "iopub.status.busy": "2026-03-14T15:47:11.741471Z", + "iopub.status.idle": "2026-03-14T15:47:11.854436Z", + "shell.execute_reply": "2026-03-14T15:47:11.853952Z" + } + }, "outputs": [], "source": [ "# SECTION 17: Summarize model selection\n", @@ -287,7 +571,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 8, diff --git a/notebooks/PPThinning.ipynb b/notebooks/PPThinning.ipynb index 1b6f4d21..1614861e 100644 --- a/notebooks/PPThinning.ipynb +++ b/notebooks/PPThinning.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "52c28645", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:17.719509Z", + "iopub.status.busy": "2026-03-14T15:47:17.719306Z", + "iopub.status.idle": "2026-03-14T15:47:19.963867Z", + "shell.execute_reply": "2026-03-14T15:47:19.963219Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: PPThinning\n", @@ -53,9 +60,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "7e5a7ce6", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:19.966588Z", + "iopub.status.busy": "2026-03-14T15:47:19.966344Z", + "iopub.status.idle": "2026-03-14T15:47:19.968744Z", + "shell.execute_reply": "2026-03-14T15:47:19.968252Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Basic Example\n", @@ -77,9 +91,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "bc5fb9e3", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:19.970181Z", + "iopub.status.busy": "2026-03-14T15:47:19.970073Z", + "iopub.status.idle": "2026-03-14T15:47:20.042230Z", + "shell.execute_reply": "2026-03-14T15:47:20.041551Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Compare Constant rate process vs. thinned process\n", @@ -100,9 +121,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "bba76669", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:20.043634Z", + "iopub.status.busy": "2026-03-14T15:47:20.043532Z", + "iopub.status.idle": "2026-03-14T15:47:20.086055Z", + "shell.execute_reply": "2026-03-14T15:47:20.085427Z" + } + }, "outputs": [], "source": [ "# SECTION 3: Simulate multiple realizations of a point process via thinning\n", @@ -119,7 +147,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 3, diff --git a/notebooks/PSTHEstimation.ipynb b/notebooks/PSTHEstimation.ipynb index 9aafe1b8..521016db 100644 --- a/notebooks/PSTHEstimation.ipynb +++ b/notebooks/PSTHEstimation.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "7a5e0ff3", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:25.550031Z", + "iopub.status.busy": "2026-03-14T15:47:25.549954Z", + "iopub.status.idle": "2026-03-14T15:47:27.410032Z", + "shell.execute_reply": "2026-03-14T15:47:27.409292Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: PSTHEstimation\n", @@ -53,9 +60,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "00a3c67b", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:27.411771Z", + "iopub.status.busy": "2026-03-14T15:47:27.411624Z", + "iopub.status.idle": "2026-03-14T15:47:27.424400Z", + "shell.execute_reply": "2026-03-14T15:47:27.423929Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Generate a known Conditional Intensity Function\n", @@ -71,9 +85,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "f257e49e", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:27.425851Z", + "iopub.status.busy": "2026-03-14T15:47:27.425755Z", + "iopub.status.idle": "2026-03-14T15:47:27.491476Z", + "shell.execute_reply": "2026-03-14T15:47:27.491082Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Estimate the PSTH with 500ms windows\n", @@ -98,7 +119,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 2, diff --git a/notebooks/SignalObjExamples.ipynb b/notebooks/SignalObjExamples.ipynb index 5666e428..30441efe 100644 --- a/notebooks/SignalObjExamples.ipynb +++ b/notebooks/SignalObjExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "7b9d4565", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:31.602902Z", + "iopub.status.busy": "2026-03-14T15:47:31.602759Z", + "iopub.status.idle": "2026-03-14T15:47:33.853160Z", + "shell.execute_reply": "2026-03-14T15:47:33.852619Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: SignalObjExamples\n", @@ -36,9 +43,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "fd174c65", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:33.854779Z", + "iopub.status.busy": "2026-03-14T15:47:33.854636Z", + "iopub.status.idle": "2026-03-14T15:47:33.926918Z", + "shell.execute_reply": "2026-03-14T15:47:33.926483Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example 1: Defining and Plotting Signals\n", @@ -62,9 +76,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "d58192a4", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:33.928355Z", + "iopub.status.busy": "2026-03-14T15:47:33.928263Z", + "iopub.status.idle": "2026-03-14T15:47:34.121478Z", + "shell.execute_reply": "2026-03-14T15:47:34.121012Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Example 2: Changing Signal Properties\n", @@ -100,9 +121,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "1310c80a", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:34.123006Z", + "iopub.status.busy": "2026-03-14T15:47:34.122932Z", + "iopub.status.idle": "2026-03-14T15:47:34.205834Z", + "shell.execute_reply": "2026-03-14T15:47:34.205401Z" + } + }, "outputs": [], "source": [ "# SECTION 3: Example 3: Resampling and Windowing SignalObjs\n", @@ -122,9 +150,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "4c2fb64c", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:34.207148Z", + "iopub.status.busy": "2026-03-14T15:47:34.207080Z", + "iopub.status.idle": "2026-03-14T15:47:34.232211Z", + "shell.execute_reply": "2026-03-14T15:47:34.231765Z" + } + }, "outputs": [], "source": [ "# SECTION 4: Example 4: SignalObj Mathematical Operations\n", @@ -152,10 +187,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "28cc2086", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:34.233365Z", + "iopub.status.busy": "2026-03-14T15:47:34.233298Z", + "iopub.status.idle": "2026-03-14T15:47:34.236103Z", + "shell.execute_reply": "2026-03-14T15:47:34.235768Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: Example 5: Spectra\n", "__tracker.new_figure('figure')\n", @@ -167,9 +220,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "8aba84d8", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:34.237111Z", + "iopub.status.busy": "2026-03-14T15:47:34.237047Z", + "iopub.status.idle": "2026-03-14T15:47:34.266574Z", + "shell.execute_reply": "2026-03-14T15:47:34.266154Z" + } + }, "outputs": [], "source": [ "# SECTION 6: Example 6: View signal variability\n", @@ -194,7 +254,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 16, diff --git a/notebooks/StimulusDecode2D.ipynb b/notebooks/StimulusDecode2D.ipynb index 2f971d4b..c1a7ab12 100644 --- a/notebooks/StimulusDecode2D.ipynb +++ b/notebooks/StimulusDecode2D.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "1e9ccf67", - "metadata": {}, + "execution_count": 1, + "id": "6f7a27a5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:38.314699Z", + "iopub.status.busy": "2026-03-14T15:47:38.314472Z", + "iopub.status.idle": "2026-03-14T15:47:40.401474Z", + "shell.execute_reply": "2026-03-14T15:47:40.400853Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: StimulusDecode2D\n", @@ -162,10 +169,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "feb17f45", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "8809d377", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:47:40.403092Z", + "iopub.status.busy": "2026-03-14T15:47:40.402942Z", + "iopub.status.idle": "2026-03-14T15:51:37.815052Z", + "shell.execute_reply": "2026-03-14T15:51:37.814532Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_cells': 80, 'decode_method': 'PPDecodeFilter', 'decode_rmse': 0.0823, 'fallback_error': ''}\n" + ] + } + ], "source": [ "# SECTION 0: 2-D Stimulus Decode\n", "# This notebook follows the MATLAB 2-D decoding workflow with simulated spatial receptive fields.\n", @@ -183,9 +205,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ad9b9210", - "metadata": {}, + "execution_count": 3, + "id": "d87abf0b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:51:37.816361Z", + "iopub.status.busy": "2026-03-14T15:51:37.816262Z", + "iopub.status.idle": "2026-03-14T15:51:38.271058Z", + "shell.execute_reply": "2026-03-14T15:51:38.270498Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Generate the random receptive fields to simulate different neurons\n", @@ -229,28 +258,26 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "33ba6aa5", - "metadata": {}, - "outputs": [], - "source": [ - "# SECTION 2: Visualize the simulated neural activity\n", - "fig = _prepare_figure(\"spikeColl.plot\", figsize=(9.0, 5.0))\n", - "axs = fig.subplots(2, 1, sharex=True)\n", - "_plot_raster(axs[0], payload[\"time_s\"], payload[\"spikes\"])\n", - "axs[0].set_title(\"Population raster\")\n", - "axs[1].plot(payload[\"time_s\"], np.mean(payload[\"spikes\"], axis=1), color=\"tab:green\", linewidth=1.2)\n", - "axs[1].set_title(\"Population firing fraction\")\n", - "axs[1].set_xlabel(\"time (s)\")\n", - "axs[1].set_ylabel(\"mean spike/bin\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f07dbd8c", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "5eddb87b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:51:38.272599Z", + "iopub.status.busy": "2026-03-14T15:51:38.272509Z", + "iopub.status.idle": "2026-03-14T15:51:38.753605Z", + "shell.execute_reply": "2026-03-14T15:51:38.753136Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", + " self._active_fig.tight_layout()\n" + ] + } + ], "source": [ "# SECTION 3: Decode the x-y trajectory\n", "fig = _prepare_figure(\"plot(x_u(1,:),x_u(2,:),'b',px,py,'k')\", figsize=(6.0, 6.0))\n", @@ -292,7 +319,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 6, @@ -303,4 +339,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/TrialConfigExamples.ipynb b/notebooks/TrialConfigExamples.ipynb index 41cae891..fc68a1f9 100644 --- a/notebooks/TrialConfigExamples.ipynb +++ b/notebooks/TrialConfigExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b3c759a5", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:51:48.689262Z", + "iopub.status.busy": "2026-03-14T15:51:48.688938Z", + "iopub.status.idle": "2026-03-14T15:51:50.982495Z", + "shell.execute_reply": "2026-03-14T15:51:50.981592Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: TrialConfigExamples\n", @@ -38,7 +45,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 0, diff --git a/notebooks/TrialExamples.ipynb b/notebooks/TrialExamples.ipynb index 059109e2..805578ac 100644 --- a/notebooks/TrialExamples.ipynb +++ b/notebooks/TrialExamples.ipynb @@ -14,10 +14,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f23e6389", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:51:59.891883Z", + "iopub.status.busy": "2026-03-14T15:51:59.891609Z", + "iopub.status.idle": "2026-03-14T15:52:02.016652Z", + "shell.execute_reply": "2026-03-14T15:52:02.016197Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'trial_duration_s': 1.0, 'num_neurons': 4, 'covariates': ['Position', 'Force'], 'history_windows': [0.0, 0.1, 0.2, 0.4]}\n" + ] + } + ], "source": [ "# nSTAT-python notebook example: TrialExamples\n", "from pathlib import Path\n", @@ -120,9 +135,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "5f01a6dd", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.018696Z", + "iopub.status.busy": "2026-03-14T15:52:02.018521Z", + "iopub.status.idle": "2026-03-14T15:52:02.020613Z", + "shell.execute_reply": "2026-03-14T15:52:02.020189Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example 1: A simple data set\n", @@ -136,10 +158,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "947fce2d", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.021967Z", + "iopub.status.busy": "2026-03-14T15:52:02.021873Z", + "iopub.status.idle": "2026-03-14T15:52:02.042679Z", + "shell.execute_reply": "2026-03-14T15:52:02.042281Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 2: Create History windows of interest\n", "fig = _figure(\"figure; h.plot\", figsize=(8.0, 2.5))\n", @@ -149,10 +191,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "948c1c5e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.043903Z", + "iopub.status.busy": "2026-03-14T15:52:02.043829Z", + "iopub.status.idle": "2026-03-14T15:52:02.140909Z", + "shell.execute_reply": "2026-03-14T15:52:02.140497Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 3: Load Covariates\n", "fig = _figure(\"figure; cc.plot\", figsize=(8.5, 5.0))\n", @@ -161,10 +221,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "addc115e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.142229Z", + "iopub.status.busy": "2026-03-14T15:52:02.142151Z", + "iopub.status.idle": "2026-03-14T15:52:02.205921Z", + "shell.execute_reply": "2026-03-14T15:52:02.205565Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: Create trial events\n", "fig = _figure(\"figure; e.plot\", figsize=(8.0, 2.3))\n", @@ -174,10 +252,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "8160b030", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.207219Z", + "iopub.status.busy": "2026-03-14T15:52:02.207144Z", + "iopub.status.idle": "2026-03-14T15:52:02.276551Z", + "shell.execute_reply": "2026-03-14T15:52:02.276199Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: Create neural Spike Train Data\n", "fig = _figure(\"figure; spikeColl.plot\", figsize=(8.5, 3.5))\n", @@ -187,10 +283,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "f15709a2", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.277839Z", + "iopub.status.busy": "2026-03-14T15:52:02.277766Z", + "iopub.status.idle": "2026-03-14T15:52:02.411214Z", + "shell.execute_reply": "2026-03-14T15:52:02.410860Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 6: Finally we have everything we need to create a Trial object.\n", "fig = _figure(\"figure; trial1.plot\", figsize=(9.0, 8.0))\n", @@ -199,10 +313,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "d25ae0c3", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.412800Z", + "iopub.status.busy": "2026-03-14T15:52:02.412694Z", + "iopub.status.idle": "2026-03-14T15:52:02.657758Z", + "shell.execute_reply": "2026-03-14T15:52:02.657393Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'masked_labels': ['x', 'f_x', '[0,0.1]', '[0.1,0.2]', '[0.2,0.4]'], 'history_covariates': ['[0,0.1]', '[0.1,0.2]', '[0.2,0.4]', '[0,0.1]']}\n" + ] + } + ], "source": [ "# SECTION 7: Mask out some of the data and plot the trial once again\n", "trial1.setCovMask([[\"Position\", \"x\"], [\"Force\", \"f_x\"]])\n", @@ -215,10 +344,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "48633477", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.659120Z", + "iopub.status.busy": "2026-03-14T15:52:02.659041Z", + "iopub.status.idle": "2026-03-14T15:52:02.660871Z", + "shell.execute_reply": "2026-03-14T15:52:02.660466Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Examples of neural spike analysis using AnalysisExamples2 (Neural Spike Analysis Toolbox) or AnalysisExamples (standard methods).\n" + ] + } + ], "source": [ "# SECTION 8: Example 2: Analyzing Trial Data\n", "print(\"Examples of neural spike analysis using AnalysisExamples2 (Neural Spike Analysis Toolbox) or AnalysisExamples (standard methods).\")" @@ -226,10 +370,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "ebaa9bdc", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:02.661967Z", + "iopub.status.busy": "2026-03-14T15:52:02.661895Z", + "iopub.status.idle": "2026-03-14T15:52:02.776447Z", + "shell.execute_reply": "2026-03-14T15:52:02.776030Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'recommended_next_notebooks': ['AnalysisExamples2', 'AnalysisExamples']}\n" + ] + } + ], "source": [ "# SECTION 9: Related analysis workflows\n", "print({\"recommended_next_notebooks\": [\"AnalysisExamples2\", \"AnalysisExamples\"]})\n", @@ -239,7 +398,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 6, diff --git a/notebooks/ValidationDataSet.ipynb b/notebooks/ValidationDataSet.ipynb index cb5a92f5..0cff1891 100644 --- a/notebooks/ValidationDataSet.ipynb +++ b/notebooks/ValidationDataSet.ipynb @@ -14,9 +14,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e1e838d2", - "metadata": {}, + "execution_count": 1, + "id": "5a19211e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:09.248334Z", + "iopub.status.busy": "2026-03-14T15:52:09.248121Z", + "iopub.status.idle": "2026-03-14T15:52:11.309244Z", + "shell.execute_reply": "2026-03-14T15:52:11.308734Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: ValidationDataSet\n", @@ -153,10 +160,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a0dd7789", - "metadata": {}, - "outputs": [], + "execution_count": 2, + "id": "de07a751", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:11.310747Z", + "iopub.status.busy": "2026-03-14T15:52:11.310610Z", + "iopub.status.idle": "2026-03-14T15:52:13.618439Z", + "shell.execute_reply": "2026-03-14T15:52:13.617897Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'ci_fast_path': False, 'constant_lambda_hz': 10.0, 'piecewise_lambda1_hz': 1.0, 'piecewise_lambda2_hz': 10.0}\n" + ] + } + ], "source": [ "# SECTION 0: Software Validation Data Set\n", "# This notebook follows the MATLAB validation helpfile; CI uses a documented short fast path while local runs use MATLAB-scale sample counts.\n", @@ -175,9 +197,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "d247fcbf", - "metadata": {}, + "execution_count": 3, + "id": "4c326ba4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:13.619641Z", + "iopub.status.busy": "2026-03-14T15:52:13.619561Z", + "iopub.status.idle": "2026-03-14T15:52:13.621227Z", + "shell.execute_reply": "2026-03-14T15:52:13.620868Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Case #1: Constant Rate Poisson Process\n", @@ -186,9 +215,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "14ae875f", - "metadata": {}, + "execution_count": 4, + "id": "0348ff8b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:13.622361Z", + "iopub.status.busy": "2026-03-14T15:52:13.622297Z", + "iopub.status.idle": "2026-03-14T15:52:13.623970Z", + "shell.execute_reply": "2026-03-14T15:52:13.623551Z" + } + }, "outputs": [], "source": [ "# SECTION 2: Generate constant-rate neural firing activity\n", @@ -198,9 +234,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "50be0fae", - "metadata": {}, + "execution_count": 5, + "id": "127d7e94", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:13.624971Z", + "iopub.status.busy": "2026-03-14T15:52:13.624904Z", + "iopub.status.idle": "2026-03-14T15:52:13.651953Z", + "shell.execute_reply": "2026-03-14T15:52:13.651530Z" + } + }, "outputs": [], "source": [ "# SECTION 3: Sanity check the ISI distribution\n", @@ -212,10 +255,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "0719a6e2", - "metadata": {}, - "outputs": [], + "execution_count": 6, + "id": "ce2f5297", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:13.653120Z", + "iopub.status.busy": "2026-03-14T15:52:13.653042Z", + "iopub.status.idle": "2026-03-14T15:52:22.050180Z", + "shell.execute_reply": "2026-03-14T15:52:22.049690Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: Setup the constant-rate analysis\n", "constant_results = Analysis.RunAnalysisForAllNeurons(constant_case[\"trial\"], constant_case[\"cfg\"], 0)\n", @@ -234,10 +295,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "3a611243", - "metadata": {}, - "outputs": [], + "execution_count": 7, + "id": "e4a199c4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:22.051439Z", + "iopub.status.busy": "2026-03-14T15:52:22.051358Z", + "iopub.status.idle": "2026-03-14T15:52:22.104361Z", + "shell.execute_reply": "2026-03-14T15:52:22.103962Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: Run the constant-rate analysis\n", "fig = _prepare_figure(\"results{1}.lambda.plot\", figsize=(10.0, 4.5))\n", @@ -256,9 +335,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "0930472e", - "metadata": {}, + "execution_count": 8, + "id": "26380744", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:22.105600Z", + "iopub.status.busy": "2026-03-14T15:52:22.105525Z", + "iopub.status.idle": "2026-03-14T15:52:22.107313Z", + "shell.execute_reply": "2026-03-14T15:52:22.106907Z" + } + }, "outputs": [], "source": [ "# SECTION 6: Case #2: Piece-wise Constant Rate Poisson Process\n", @@ -269,10 +355,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c5d601a1", - "metadata": {}, - "outputs": [], + "execution_count": 9, + "id": "cf2c6402", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:22.108355Z", + "iopub.status.busy": "2026-03-14T15:52:22.108287Z", + "iopub.status.idle": "2026-03-14T15:52:22.327920Z", + "shell.execute_reply": "2026-03-14T15:52:22.327199Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 7: Generate the piecewise-rate spike trains\n", "fig = _prepare_figure(\"plot(spikeTimes1, spikeTimes2)\", figsize=(10.0, 4.5))\n", @@ -299,9 +403,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "6c4195eb", - "metadata": {}, + "execution_count": 10, + "id": "25dfb2e5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:22.329358Z", + "iopub.status.busy": "2026-03-14T15:52:22.329246Z", + "iopub.status.idle": "2026-03-14T15:52:50.344851Z", + "shell.execute_reply": "2026-03-14T15:52:50.344322Z" + } + }, "outputs": [], "source": [ "# SECTION 8: Setup the piecewise-rate analysis\n", @@ -310,10 +421,28 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e5968267", - "metadata": {}, - "outputs": [], + "execution_count": 11, + "id": "113d3272", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:50.346839Z", + "iopub.status.busy": "2026-03-14T15:52:50.346694Z", + "iopub.status.idle": "2026-03-14T15:52:50.551367Z", + "shell.execute_reply": "2026-03-14T15:52:50.550906Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 9: Run the piecewise-rate analysis\n", "fig = _prepare_figure(\"results{1}.lambda.plot\", figsize=(10.0, 4.5))\n", @@ -345,9 +474,16 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "989a4bd1", - "metadata": {}, + "execution_count": 12, + "id": "1d9d4cce", + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:52:50.552962Z", + "iopub.status.busy": "2026-03-14T15:52:50.552800Z", + "iopub.status.idle": "2026-03-14T15:52:50.960765Z", + "shell.execute_reply": "2026-03-14T15:52:50.960323Z" + } + }, "outputs": [], "source": [ "# SECTION 10: Compare the results across the two neurons\n", @@ -379,7 +515,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 10, diff --git a/notebooks/mEPSCAnalysis.ipynb b/notebooks/mEPSCAnalysis.ipynb index 9dc5dec0..25ceb020 100644 --- a/notebooks/mEPSCAnalysis.ipynb +++ b/notebooks/mEPSCAnalysis.ipynb @@ -6,10 +6,10 @@ "id": "b499a26e", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:49.400898Z", - "iopub.status.busy": "2026-03-07T21:18:49.400810Z", - "iopub.status.idle": "2026-03-07T21:18:51.082951Z", - "shell.execute_reply": "2026-03-07T21:18:51.082481Z" + "iopub.execute_input": "2026-03-14T15:52:54.632207Z", + "iopub.status.busy": "2026-03-14T15:52:54.632112Z", + "iopub.status.idle": "2026-03-14T15:52:55.984222Z", + "shell.execute_reply": "2026-03-14T15:52:55.983798Z" } }, "outputs": [], @@ -68,10 +68,10 @@ "id": "ca17ef47", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:51.084508Z", - "iopub.status.busy": "2026-03-07T21:18:51.084357Z", - "iopub.status.idle": "2026-03-07T21:18:51.086463Z", - "shell.execute_reply": "2026-03-07T21:18:51.085971Z" + "iopub.execute_input": "2026-03-14T15:52:55.985858Z", + "iopub.status.busy": "2026-03-14T15:52:55.985730Z", + "iopub.status.idle": "2026-03-14T15:52:55.987730Z", + "shell.execute_reply": "2026-03-14T15:52:55.987329Z" } }, "outputs": [], @@ -94,10 +94,10 @@ "id": "17a0e642", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:51.087750Z", - "iopub.status.busy": "2026-03-07T21:18:51.087638Z", - "iopub.status.idle": "2026-03-07T21:18:51.330625Z", - "shell.execute_reply": "2026-03-07T21:18:51.330189Z" + "iopub.execute_input": "2026-03-14T15:52:55.988838Z", + "iopub.status.busy": "2026-03-14T15:52:55.988766Z", + "iopub.status.idle": "2026-03-14T15:52:56.718045Z", + "shell.execute_reply": "2026-03-14T15:52:56.717490Z" } }, "outputs": [ @@ -105,7 +105,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'constant_events': 573, 'AIC': [4645.786426156066]}\n" + "{'constant_events': 573, 'AIC': [3554.3793854525825]}\n" ] } ], @@ -137,10 +137,10 @@ "id": "b5423ec1", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:51.331896Z", - "iopub.status.busy": "2026-03-07T21:18:51.331792Z", - "iopub.status.idle": "2026-03-07T21:18:51.333567Z", - "shell.execute_reply": "2026-03-07T21:18:51.333237Z" + "iopub.execute_input": "2026-03-14T15:52:56.719425Z", + "iopub.status.busy": "2026-03-14T15:52:56.719321Z", + "iopub.status.idle": "2026-03-14T15:52:56.721472Z", + "shell.execute_reply": "2026-03-14T15:52:56.720877Z" } }, "outputs": [], @@ -159,13 +159,22 @@ "id": "68209c75", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:51.334835Z", - "iopub.status.busy": "2026-03-07T21:18:51.334749Z", - "iopub.status.idle": "2026-03-07T21:18:52.117456Z", - "shell.execute_reply": "2026-03-07T21:18:52.116832Z" + "iopub.execute_input": "2026-03-14T15:52:56.722714Z", + "iopub.status.busy": "2026-03-14T15:52:56.722621Z", + "iopub.status.idle": "2026-03-14T15:52:57.576758Z", + "shell.execute_reply": "2026-03-14T15:52:57.576195Z" } }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: Tight layout not applied. tight_layout cannot make Axes width small enough to accommodate all Axes decorations\n", + " self._active_fig.tight_layout()\n" + ] + } + ], "source": [ "# SECTION 4: Data Visualization\n", "washout1 = np.loadtxt(DATA_DIR / \"mEPSCs\" / \"washout1.txt\", skiprows=1)\n", @@ -205,10 +214,10 @@ "id": "a442059e", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:52.118844Z", - "iopub.status.busy": "2026-03-07T21:18:52.118757Z", - "iopub.status.idle": "2026-03-07T21:18:52.120683Z", - "shell.execute_reply": "2026-03-07T21:18:52.120139Z" + "iopub.execute_input": "2026-03-14T15:52:57.578269Z", + "iopub.status.busy": "2026-03-14T15:52:57.578172Z", + "iopub.status.idle": "2026-03-14T15:52:57.580076Z", + "shell.execute_reply": "2026-03-14T15:52:57.579672Z" } }, "outputs": [], @@ -229,10 +238,10 @@ "id": "5a11a092", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:52.121960Z", - "iopub.status.busy": "2026-03-07T21:18:52.121862Z", - "iopub.status.idle": "2026-03-07T21:18:54.902008Z", - "shell.execute_reply": "2026-03-07T21:18:54.901529Z" + "iopub.execute_input": "2026-03-14T15:52:57.581205Z", + "iopub.status.busy": "2026-03-14T15:52:57.581125Z", + "iopub.status.idle": "2026-03-14T15:53:07.137554Z", + "shell.execute_reply": "2026-03-14T15:53:07.137142Z" } }, "outputs": [ @@ -262,13 +271,24 @@ "id": "b8956c4d", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:54.903552Z", - "iopub.status.busy": "2026-03-07T21:18:54.903387Z", - "iopub.status.idle": "2026-03-07T21:18:55.448887Z", - "shell.execute_reply": "2026-03-07T21:18:55.448284Z" + "iopub.execute_input": "2026-03-14T15:53:07.138818Z", + "iopub.status.busy": "2026-03-14T15:53:07.138730Z", + "iopub.status.idle": "2026-03-14T15:53:08.496758Z", + "shell.execute_reply": "2026-03-14T15:53:08.495920Z" } }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/fit.py:1174: UserWarning: Tight layout not applied. tight_layout cannot make Axes width small enough to accommodate all Axes decorations\n", + " fig.tight_layout()\n", + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: Tight layout not applied. tight_layout cannot make Axes width small enough to accommodate all Axes decorations\n", + " self._active_fig.tight_layout()\n" + ] + } + ], "source": [ "# SECTION 7: Perform Analysis\n", "fig = __tracker.new_figure(\"washout-analysis-results\")\n", @@ -285,10 +305,10 @@ "id": "02d87e14", "metadata": { "execution": { - "iopub.execute_input": "2026-03-07T21:18:55.450262Z", - "iopub.status.busy": "2026-03-07T21:18:55.450153Z", - "iopub.status.idle": "2026-03-07T21:18:55.453252Z", - "shell.execute_reply": "2026-03-07T21:18:55.452738Z" + "iopub.execute_input": "2026-03-14T15:53:08.498695Z", + "iopub.status.busy": "2026-03-14T15:53:08.498555Z", + "iopub.status.idle": "2026-03-14T15:53:08.501769Z", + "shell.execute_reply": "2026-03-14T15:53:08.501291Z" } }, "outputs": [], @@ -362,7 +382,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.4" + "version": "3.12.9" }, "nstat": { "expected_figures": 4, diff --git a/notebooks/nSTATPaperExamples.ipynb b/notebooks/nSTATPaperExamples.ipynb index 92f4d7ab..ee39128d 100644 --- a/notebooks/nSTATPaperExamples.ipynb +++ b/notebooks/nSTATPaperExamples.ipynb @@ -14,10 +14,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "64f99156", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:12.343005Z", + "iopub.status.busy": "2026-03-14T15:53:12.342862Z", + "iopub.status.idle": "2026-03-14T15:53:16.360311Z", + "shell.execute_reply": "2026-03-14T15:53:16.359787Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'dataset_root': '/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/data_cache/nstat_data', 'paper_examples_loaded': 8}\n" + ] + } + ], "source": [ "# nSTAT-python notebook example: nSTATPaperExamples\n", "from pathlib import Path\n", @@ -72,10 +87,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "cf03a39b", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.361564Z", + "iopub.status.busy": "2026-03-14T15:53:16.361420Z", + "iopub.status.idle": "2026-03-14T15:53:16.363273Z", + "shell.execute_reply": "2026-03-14T15:53:16.362921Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'const_condition_spikes': 573.0, 'const_model_aic': 3878.21450302445, 'const_model_bic': 3884.9477748694585, 'constant_acf_ci': 0.08188017465629394, 'decreasing_condition_spikes': 1870.0, 'piecewise_model_aic': 17581.121678089126, 'piecewise_model_bic': 17609.2027486881, 'piecewise_history_model_aic': 17585.640410038308, 'piecewise_history_model_bic': 17707.32504930054, 'dt_seconds': 0.01}\n" + ] + } + ], "source": [ "# SECTION 1: Experiment 1\n", "print(exp1_summary)" @@ -83,10 +113,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "facb48a1", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.364317Z", + "iopub.status.busy": "2026-03-14T15:53:16.364247Z", + "iopub.status.idle": "2026-03-14T15:53:16.382375Z", + "shell.execute_reply": "2026-03-14T15:53:16.382049Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Constant Mg condition: homogeneous Poisson fit')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 2: Constant Magnesium Concentration - Constant rate poisson\n", "fig = _fig(\"experiment1 constant rate\", figsize=(9.0, 4.0))\n", @@ -99,10 +147,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "b3453ddd", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.383620Z", + "iopub.status.busy": "2026-03-14T15:53:16.383546Z", + "iopub.status.idle": "2026-03-14T15:53:16.385498Z", + "shell.execute_reply": "2026-03-14T15:53:16.385074Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'decreasing_condition_spikes': 1870.0, 'piecewise_model_aic': 17581.122}\n" + ] + } + ], "source": [ "# SECTION 3: Varying Magnesium Concentration - Piecewise Constant rate poisson\n", "print({\"decreasing_condition_spikes\": exp1_summary[\"decreasing_condition_spikes\"], \"piecewise_model_aic\": round(float(exp1_summary[\"piecewise_model_aic\"]), 3)})" @@ -110,10 +173,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "6a1a4315", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.386566Z", + "iopub.status.busy": "2026-03-14T15:53:16.386485Z", + "iopub.status.idle": "2026-03-14T15:53:16.533405Z", + "shell.execute_reply": "2026-03-14T15:53:16.532596Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 4: Data Visualization\n", "fig = _fig(\"experiment1 washout raster and rates\", figsize=(10.0, 5.5))\n", @@ -135,10 +216,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "9bf5cb26", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.537209Z", + "iopub.status.busy": "2026-03-14T15:53:16.536864Z", + "iopub.status.idle": "2026-03-14T15:53:16.651196Z", + "shell.execute_reply": "2026-03-14T15:53:16.650395Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Constant-condition KS plot')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 5: Define Covariates for the analysis\n", "fig = _fig(\"experiment1 constant ks\", figsize=(6.0, 5.0))\n", @@ -155,10 +254,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "197eb8cd", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.652799Z", + "iopub.status.busy": "2026-03-14T15:53:16.652662Z", + "iopub.status.idle": "2026-03-14T15:53:16.710268Z", + "shell.execute_reply": "2026-03-14T15:53:16.709737Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Sequential correlation under constant Mg')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 6: Define how we want to analyze the data\n", "fig = _fig(\"experiment1 constant acf\", figsize=(7.0, 4.0))\n", @@ -173,10 +290,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "53c12b4c", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.711889Z", + "iopub.status.busy": "2026-03-14T15:53:16.711786Z", + "iopub.status.idle": "2026-03-14T15:53:16.763423Z", + "shell.execute_reply": "2026-03-14T15:53:16.762994Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Experiment 1 model comparison')" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 7: Compare constant-rate and piecewise-rate fits\n", "fig = _fig(\"experiment1 model summary\", figsize=(7.5, 4.0))\n", @@ -191,10 +326,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "b6751400", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.764705Z", + "iopub.status.busy": "2026-03-14T15:53:16.764626Z", + "iopub.status.idle": "2026-03-14T15:53:16.766478Z", + "shell.execute_reply": "2026-03-14T15:53:16.766095Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'n_samples': 51000.0, 'model1_aic': 9597.1177937183, 'model2_aic': 1615663214437.8765, 'model3_aic': 1618939900919.947, 'model1_bic': 9605.957374630007, 'model2_bic': 1615663214464.3953, 'model3_bic': 1618939900990.6636, 'peak_lag_seconds': 0.11900000000000001}\n" + ] + } + ], "source": [ "# SECTION 8: Experiment 2\n", "print(exp2_summary)" @@ -202,10 +352,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "be05f22d", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.767400Z", + "iopub.status.busy": "2026-03-14T15:53:16.767330Z", + "iopub.status.idle": "2026-03-14T15:53:16.841133Z", + "shell.execute_reply": "2026-03-14T15:53:16.840534Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 9: Load the explicit-stimulus dataset\n", "fig = _fig(\"experiment2 stimulus and spikes\", figsize=(10.0, 5.5))\n", @@ -221,10 +389,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "f704736c", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.842747Z", + "iopub.status.busy": "2026-03-14T15:53:16.842527Z", + "iopub.status.idle": "2026-03-14T15:53:16.937296Z", + "shell.execute_reply": "2026-03-14T15:53:16.936743Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Stimulus lag search')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 10: Stimulus-lag search\n", "fig = _fig(\"experiment2 xcorr\", figsize=(7.0, 4.0))\n", @@ -237,10 +423,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "620d4f28", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.938686Z", + "iopub.status.busy": "2026-03-14T15:53:16.938594Z", + "iopub.status.idle": "2026-03-14T15:53:16.992109Z", + "shell.execute_reply": "2026-03-14T15:53:16.991529Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'BIC')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 11: Model comparison with stimulus effects\n", "fig = _fig(\"experiment2 aic bic\", figsize=(8.5, 4.0))\n", @@ -256,10 +460,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "0803cc01", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:16.993566Z", + "iopub.status.busy": "2026-03-14T15:53:16.993448Z", + "iopub.status.idle": "2026-03-14T15:53:17.059489Z", + "shell.execute_reply": "2026-03-14T15:53:17.059001Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Experiment 2 KS diagnostics')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 12: KS diagnostics\n", "fig = _fig(\"experiment2 ks compare\", figsize=(6.5, 5.0))\n", @@ -278,10 +500,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "e8e2cecc", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.060682Z", + "iopub.status.busy": "2026-03-14T15:53:17.060602Z", + "iopub.status.idle": "2026-03-14T15:53:17.127053Z", + "shell.execute_reply": "2026-03-14T15:53:17.126651Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'history windows')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 13: History-window scan\n", "fig = _fig(\"experiment2 history scan\", figsize=(8.5, 7.0))\n", @@ -298,10 +538,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "86253034", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.128502Z", + "iopub.status.busy": "2026-03-14T15:53:17.128418Z", + "iopub.status.idle": "2026-03-14T15:53:17.215039Z", + "shell.execute_reply": "2026-03-14T15:53:17.214554Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Experiment 2 coefficient intervals')" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 14: Coefficient summaries\n", "fig = _fig(\"experiment2 coefficients\", figsize=(9.0, 4.5))\n", @@ -318,10 +576,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "214e5be7", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.216358Z", + "iopub.status.busy": "2026-03-14T15:53:17.216274Z", + "iopub.status.idle": "2026-03-14T15:53:17.218116Z", + "shell.execute_reply": "2026-03-14T15:53:17.217782Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_trials': 20.0, 'psth_peak_hz': 124.99999999999994, 'psth_mean_hz': 53.749999999999986, 'total_spikes': 1075.0}\n" + ] + } + ], "source": [ "# SECTION 15: Experiment 3\n", "print(exp3_summary)" @@ -329,10 +602,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "f39efc64", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.219197Z", + "iopub.status.busy": "2026-03-14T15:53:17.219123Z", + "iopub.status.idle": "2026-03-14T15:53:17.266402Z", + "shell.execute_reply": "2026-03-14T15:53:17.266017Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Experiment 3 true conditional intensity')" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 16: Simulated PSTH setup\n", "fig = _fig(\"experiment3 true rate\", figsize=(9.0, 4.0))\n", @@ -345,10 +636,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "39340a3b", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.267460Z", + "iopub.status.busy": "2026-03-14T15:53:17.267389Z", + "iopub.status.idle": "2026-03-14T15:53:17.363755Z", + "shell.execute_reply": "2026-03-14T15:53:17.363424Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 17: PSTH estimate\n", "fig = _fig(\"experiment3 psth\", figsize=(9.0, 5.0))\n", @@ -363,10 +672,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "e80bdf17", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.365024Z", + "iopub.status.busy": "2026-03-14T15:53:17.364945Z", + "iopub.status.idle": "2026-03-14T15:53:17.366889Z", + "shell.execute_reply": "2026-03-14T15:53:17.366489Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_trials': 25.0, 'num_time_bins': 50.0, 'state_rmse': 118.29190338935122, 'ci_coverage': 1.0, 'mean_qhat': 0.01646313440905654, 'mean_gammahat': -1.8798710833855103, 'log_likelihood': -0.19416505639708248}\n" + ] + } + ], "source": [ "# SECTION 18: Experiment 3b\n", "print(exp3b_summary)" @@ -374,10 +698,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "d3f429ac", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.367890Z", + "iopub.status.busy": "2026-03-14T15:53:17.367809Z", + "iopub.status.idle": "2026-03-14T15:53:17.432524Z", + "shell.execute_reply": "2026-03-14T15:53:17.432086Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time bin')" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 19: SSGLM state estimates\n", "fig = _fig(\"experiment3b state estimates\", figsize=(10.0, 5.0))\n", @@ -391,10 +733,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "9e327384", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.433791Z", + "iopub.status.busy": "2026-03-14T15:53:17.433701Z", + "iopub.status.idle": "2026-03-14T15:53:17.535469Z", + "shell.execute_reply": "2026-03-14T15:53:17.534921Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Mean Qhat across models')" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 20: SSGLM confidence intervals\n", "fig = _fig(\"experiment3b ci width\", figsize=(8.5, 4.5))\n", @@ -407,10 +767,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "2fd1d209", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.536935Z", + "iopub.status.busy": "2026-03-14T15:53:17.536819Z", + "iopub.status.idle": "2026-03-14T15:53:17.620928Z", + "shell.execute_reply": "2026-03-14T15:53:17.620388Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'gammahatAll')" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 21: SSGLM gamma summaries\n", "fig = _fig(\"experiment3b gamma\", figsize=(8.5, 4.5))\n", @@ -423,10 +801,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "8150177d", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.622168Z", + "iopub.status.busy": "2026-03-14T15:53:17.622057Z", + "iopub.status.idle": "2026-03-14T15:53:17.624007Z", + "shell.execute_reply": "2026-03-14T15:53:17.623569Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_cells_fit': 8.0, 'mean_delta_aic_gaussian_minus_zernike': 87.65071800242686, 'mean_delta_bic_gaussian_minus_zernike': 44.35785401057149}\n" + ] + } + ], "source": [ "# SECTION 22: Experiment 4\n", "print(exp4_summary)" @@ -434,10 +827,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "19dabcf1", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.625328Z", + "iopub.status.busy": "2026-03-14T15:53:17.625236Z", + "iopub.status.idle": "2026-03-14T15:53:17.702686Z", + "shell.execute_reply": "2026-03-14T15:53:17.702244Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Animal 1 place-cell comparison')" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 23: Place-cell model comparison for Animal 1\n", "fig = _fig(\"experiment4 animal1 delta aic\", figsize=(7.5, 4.0))\n", @@ -450,10 +861,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "21ef2693", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.703873Z", + "iopub.status.busy": "2026-03-14T15:53:17.703786Z", + "iopub.status.idle": "2026-03-14T15:53:17.749203Z", + "shell.execute_reply": "2026-03-14T15:53:17.748742Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Animal 2 place-cell comparison')" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 24: Place-cell model comparison for Animal 2\n", "fig = _fig(\"experiment4 animal2 delta bic\", figsize=(7.5, 4.0))\n", @@ -466,10 +895,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "549f0f6b", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.751047Z", + "iopub.status.busy": "2026-03-14T15:53:17.750895Z", + "iopub.status.idle": "2026-03-14T15:53:17.798703Z", + "shell.execute_reply": "2026-03-14T15:53:17.798223Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0.92, 'Gaussian place-field estimate')" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 25: Place-field mesh for representative neuron\n", "fig = _fig(\"experiment4 gaussian mesh\", figsize=(9.0, 6.5))\n", @@ -480,10 +927,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "3e3bb9b7", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.800106Z", + "iopub.status.busy": "2026-03-14T15:53:17.800017Z", + "iopub.status.idle": "2026-03-14T15:53:17.959702Z", + "shell.execute_reply": "2026-03-14T15:53:17.959326Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0.92, 'Zernike place-field estimate')" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 26: Zernike place-field mesh\n", "fig = _fig(\"experiment4 zernike mesh\", figsize=(9.0, 6.5))\n", @@ -494,10 +959,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "96c64e62", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.961250Z", + "iopub.status.busy": "2026-03-14T15:53:17.961149Z", + "iopub.status.idle": "2026-03-14T15:53:17.963084Z", + "shell.execute_reply": "2026-03-14T15:53:17.962613Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_cells': 20.0, 'decode_rmse': 0.6531227068287989}\n" + ] + } + ], "source": [ "# SECTION 27: Experiment 5\n", "print(exp5_summary)" @@ -505,12 +985,30 @@ }, { "cell_type": "code", + "execution_count": 29, "id": "b82b3148", - "metadata": {}, - "outputs": [], - "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:17.964225Z", + "iopub.status.busy": "2026-03-14T15:53:17.964144Z", + "iopub.status.idle": "2026-03-14T15:53:18.162204Z", + "shell.execute_reply": "2026-03-14T15:53:18.161847Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# SECTION 28: CIF setup \u2014 stimulus, conditional intensity, and spike raster\n", + "# SECTION 28: CIF setup — stimulus, conditional intensity, and spike raster\n", "fig = _fig(\"experiment5 cif setup\", figsize=(10.0, 8.0))\n", "axs = fig.subplots(3, 1, sharex=True)\n", "# Panel 1: driving stimulus\n", @@ -531,10 +1029,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "b88f5c9e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.163826Z", + "iopub.status.busy": "2026-03-14T15:53:18.163727Z", + "iopub.status.idle": "2026-03-14T15:53:18.277354Z", + "shell.execute_reply": "2026-03-14T15:53:18.276855Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Experiment 5 adaptive decoding')" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 29: 1-D decoding workflow\n", "fig = _fig(\"experiment5 stimulus decode\", figsize=(9.0, 4.5))\n", @@ -549,10 +1065,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "b751e28e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.278798Z", + "iopub.status.busy": "2026-03-14T15:53:18.278701Z", + "iopub.status.idle": "2026-03-14T15:53:18.280607Z", + "shell.execute_reply": "2026-03-14T15:53:18.280132Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_cells': 30.0, 'num_samples': 2002.0, 'decode_rmse_x': 0.17559231283627283, 'decode_rmse_y': 0.14050174855856312}\n" + ] + } + ], "source": [ "# SECTION 30: Experiment 5b\n", "print(exp5b_summary)" @@ -560,12 +1091,30 @@ }, { "cell_type": "code", + "execution_count": 32, "id": "8b394726", - "metadata": {}, - "outputs": [], - "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.281650Z", + "iopub.status.busy": "2026-03-14T15:53:18.281577Z", + "iopub.status.idle": "2026-03-14T15:53:18.377038Z", + "shell.execute_reply": "2026-03-14T15:53:18.376560Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Neural raster')" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# SECTION 31: Reach setup \u2014 trajectory, kinematics, and spike raster\n", + "# SECTION 31: Reach setup — trajectory, kinematics, and spike raster\n", "fig = _fig(\"experiment5b reach setup\", figsize=(12.0, 8.0))\n", "axs = fig.subplots(2, 2, gridspec_kw={\"hspace\": 0.35, \"wspace\": 0.3})\n", "# (0,0) 2-D reach path\n", @@ -598,10 +1147,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "48493368", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.378259Z", + "iopub.status.busy": "2026-03-14T15:53:18.378179Z", + "iopub.status.idle": "2026-03-14T15:53:18.587247Z", + "shell.execute_reply": "2026-03-14T15:53:18.586775Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/iahncajigas/Library/CloudStorage/Dropbox/Claude/nSTAT-python/nstat/notebook_figures.py:42: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", + " self._active_fig.tight_layout()\n" + ] + }, + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 32: Goal-directed 2-D decode\n", "fig = _fig(\"experiment5b goal decode\", figsize=(9.5, 4.5))\n", @@ -617,10 +1192,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "id": "fdeed99c", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.588638Z", + "iopub.status.busy": "2026-03-14T15:53:18.588536Z", + "iopub.status.idle": "2026-03-14T15:53:18.674602Z", + "shell.execute_reply": "2026-03-14T15:53:18.674215Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 33: Free-model 2-D decode\n", "fig = _fig(\"experiment5b free decode\", figsize=(9.5, 4.5))\n", @@ -636,10 +1229,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "98b8c650", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.675762Z", + "iopub.status.busy": "2026-03-14T15:53:18.675682Z", + "iopub.status.idle": "2026-03-14T15:53:18.677549Z", + "shell.execute_reply": "2026-03-14T15:53:18.677149Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'num_samples': 3000.0, 'num_cells': 24.0, 'state_accuracy': 0.899, 'decode_rmse_x': 0.21622609499000853, 'decode_rmse_y': 0.17204447423928235}\n" + ] + } + ], "source": [ "# SECTION 34: Experiment 6\n", "print(exp6_summary)" @@ -647,12 +1255,30 @@ }, { "cell_type": "code", + "execution_count": 36, "id": "0d85b9c5", - "metadata": {}, - "outputs": [], - "execution_count": null, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.678901Z", + "iopub.status.busy": "2026-03-14T15:53:18.678826Z", + "iopub.status.idle": "2026-03-14T15:53:18.773200Z", + "shell.execute_reply": "2026-03-14T15:53:18.772826Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Neural raster')" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# SECTION 35: Hybrid setup \u2014 trajectory, state, kinematics, and spike raster\n", + "# SECTION 35: Hybrid setup — trajectory, state, kinematics, and spike raster\n", "fig = _fig(\"experiment6 hybrid setup\", figsize=(12.0, 8.0))\n", "axs = fig.subplots(2, 2, gridspec_kw={\"hspace\": 0.35, \"wspace\": 0.3})\n", "# (0,0) 2-D reach path coloured by state\n", @@ -684,10 +1310,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "id": "01efa943", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.774694Z", + "iopub.status.busy": "2026-03-14T15:53:18.774591Z", + "iopub.status.idle": "2026-03-14T15:53:18.976392Z", + "shell.execute_reply": "2026-03-14T15:53:18.975907Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 36: Hybrid-filter simulation\n", "fig = _fig(\"experiment6 state probabilities\", figsize=(9.5, 4.5))\n", @@ -702,10 +1346,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 38, "id": "fbcd95b6", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:18.977637Z", + "iopub.status.busy": "2026-03-14T15:53:18.977559Z", + "iopub.status.idle": "2026-03-14T15:53:19.050329Z", + "shell.execute_reply": "2026-03-14T15:53:19.049971Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'time (s)')" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 37: Hybrid-filter decoded positions\n", "fig = _fig(\"experiment6 decoded positions\", figsize=(9.5, 4.5))\n", @@ -721,10 +1383,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "2c1cf92e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:19.051669Z", + "iopub.status.busy": "2026-03-14T15:53:19.051591Z", + "iopub.status.idle": "2026-03-14T15:53:19.122670Z", + "shell.execute_reply": "2026-03-14T15:53:19.122342Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Decoding summary across paper examples')" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 38: Canonical paper-example gallery summary\n", "fig = _fig(\"paper gallery summary\", figsize=(8.5, 4.5))\n", @@ -739,10 +1419,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "id": "bb79988e", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:19.123934Z", + "iopub.status.busy": "2026-03-14T15:53:19.123861Z", + "iopub.status.idle": "2026-03-14T15:53:19.169435Z", + "shell.execute_reply": "2026-03-14T15:53:19.168959Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Paper-example dataset scale')" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# SECTION 39: Dataset-backed parity summary\n", "fig = _fig(\"paper dataset summary\", figsize=(8.5, 4.5))\n", @@ -762,10 +1460,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "id": "9aa50b57", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:19.170620Z", + "iopub.status.busy": "2026-03-14T15:53:19.170538Z", + "iopub.status.idle": "2026-03-14T15:53:19.207497Z", + "shell.execute_reply": "2026-03-14T15:53:19.206984Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'experiment1_piecewise_history_aic': 17585.64, 'experiment2_peak_lag_ms': 119.0, 'experiment4_mean_delta_aic': 87.651, 'experiment6_state_accuracy': 0.899}\n" + ] + } + ], "source": [ "# SECTION 40: Final summary\n", "print(\n", @@ -782,7 +1495,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 26, @@ -793,4 +1515,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/nSpikeTrainExamples.ipynb b/notebooks/nSpikeTrainExamples.ipynb index f6b44cae..7a3db256 100644 --- a/notebooks/nSpikeTrainExamples.ipynb +++ b/notebooks/nSpikeTrainExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f95178ba", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:23.356248Z", + "iopub.status.busy": "2026-03-14T15:53:23.356121Z", + "iopub.status.idle": "2026-03-14T15:53:25.592984Z", + "shell.execute_reply": "2026-03-14T15:53:25.592255Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: nSpikeTrainExamples\n", @@ -35,9 +42,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "82c6de48", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:25.594815Z", + "iopub.status.busy": "2026-03-14T15:53:25.594657Z", + "iopub.status.idle": "2026-03-14T15:53:25.750084Z", + "shell.execute_reply": "2026-03-14T15:53:25.749436Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Example 1: Using the nspikeTrain Class\n", @@ -60,7 +74,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 4, diff --git a/notebooks/nstCollExamples.ipynb b/notebooks/nstCollExamples.ipynb index 835207c9..0dd14e53 100644 --- a/notebooks/nstCollExamples.ipynb +++ b/notebooks/nstCollExamples.ipynb @@ -2,9 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f89859e2", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:29.854047Z", + "iopub.status.busy": "2026-03-14T15:53:29.853918Z", + "iopub.status.idle": "2026-03-14T15:53:32.033072Z", + "shell.execute_reply": "2026-03-14T15:53:32.032584Z" + } + }, "outputs": [], "source": [ "# nSTAT-python notebook example: nstCollExamples\n", @@ -51,9 +58,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "532cfdf4", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-03-14T15:53:32.034705Z", + "iopub.status.busy": "2026-03-14T15:53:32.034559Z", + "iopub.status.idle": "2026-03-14T15:53:32.130414Z", + "shell.execute_reply": "2026-03-14T15:53:32.129939Z" + } + }, "outputs": [], "source": [ "# SECTION 1: Test the nstColl Class\n", @@ -82,7 +96,16 @@ ], "metadata": { "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" }, "nstat": { "expected_figures": 3, diff --git a/nstat/data/paperHybridFilterExample.mat b/nstat/data/paperHybridFilterExample.mat new file mode 100644 index 00000000..355e2232 Binary files /dev/null and b/nstat/data/paperHybridFilterExample.mat differ diff --git a/nstat/decoding_algorithms.py b/nstat/decoding_algorithms.py index 15bcd51d..18ecb70d 100644 --- a/nstat/decoding_algorithms.py +++ b/nstat/decoding_algorithms.py @@ -2107,20 +2107,24 @@ def PPHybridFilterLinear( X_u[model_index][:, time_index] = upd_x W_u[model_index][:, :, time_index] = upd_W - # Likelihood via Laplace approximation (Srinivasan et al. 2007). - # Compute sqrt(det(W_u)) / sqrt(det(W_p)) as sqrt(det(W_u)/det(W_p)) - # to avoid a fixed floor that destroys the ratio for - # high-dimensional models with tiny absolute determinants. - det_upd = np.linalg.det(upd_W) - det_pred = np.linalg.det(pred_W) - if det_pred > 0.0 and det_upd >= 0.0: - det_ratio = np.sqrt(det_upd / det_pred) - elif det_upd == 0.0 and det_pred == 0.0: - det_ratio = 1.0 + # Likelihood (Step 5): Laplace approximation + # p(n|s) = sqrt(det(W_u)/det(W_p)) * prod(exp(dN*log(lambda) - lambda)) + # Use log-determinants for numerical stability with high-dimensional + # covariance matrices whose determinants can be extremely small (e.g. + # 1e-46 for 6-D states with tiny eigenvalues). MATLAB computes + # sqrt(det(W_u))/sqrt(det(W_p)) directly without any clamping. + sign_u, logdet_u = np.linalg.slogdet(upd_W) + sign_p, logdet_p = np.linalg.slogdet(pred_W) + lambda_flat = lambda_delta.reshape(-1) + log_spike = np.sum( + obs[:, time_index] * np.log(np.where(lambda_flat > 0, lambda_flat, 1e-300)) + - lambda_flat + ) + if sign_u > 0 and sign_p > 0: + log_likelihood = 0.5 * (logdet_u - logdet_p) + log_spike + likelihoods[model_index] = float(np.exp(log_likelihood)) else: - det_ratio = 0.0 - log_term = np.sum(obs[:, time_index] * np.log(np.clip(lambda_delta.reshape(-1), 1e-12, np.inf)) - lambda_delta.reshape(-1)) - likelihoods[model_index] = float(det_ratio * np.exp(np.clip(log_term, -200.0, 50.0))) + likelihoods[model_index] = 0.0 finite_likelihoods = likelihoods.copy() finite_likelihoods[~np.isfinite(finite_likelihoods)] = 0.0 @@ -2287,20 +2291,24 @@ def PPHybridFilter(A, Q, p_ij, Mu0, dN, lambdaCIFColl, binwidth=0.001, x0=None, X_u[model_index][:, time_index] = upd_x W_u[model_index][:, :, time_index] = upd_W - # Likelihood via Laplace approximation (Srinivasan et al. 2007). - # Compute sqrt(det(W_u)) / sqrt(det(W_p)) as sqrt(det(W_u)/det(W_p)) - # to avoid a fixed floor that destroys the ratio for - # high-dimensional models with tiny absolute determinants. - det_upd = np.linalg.det(upd_W) - det_pred = np.linalg.det(pred_W) - if det_pred > 0.0 and det_upd >= 0.0: - det_ratio = np.sqrt(det_upd / det_pred) - elif det_upd == 0.0 and det_pred == 0.0: - det_ratio = 1.0 + # Likelihood (Step 5): Laplace approximation + # p(n|s) = sqrt(det(W_u)/det(W_p)) * prod(exp(dN*log(lambda) - lambda)) + # Use log-determinants for numerical stability with high-dimensional + # covariance matrices whose determinants can be extremely small (e.g. + # 1e-46 for 6-D states with tiny eigenvalues). MATLAB computes + # sqrt(det(W_u))/sqrt(det(W_p)) directly without any clamping. + sign_u, logdet_u = np.linalg.slogdet(upd_W) + sign_p, logdet_p = np.linalg.slogdet(pred_W) + lambda_flat = lambda_delta.reshape(-1) + log_spike = np.sum( + obs[:, time_index] * np.log(np.where(lambda_flat > 0, lambda_flat, 1e-300)) + - lambda_flat + ) + if sign_u > 0 and sign_p > 0: + log_likelihood = 0.5 * (logdet_u - logdet_p) + log_spike + likelihoods[model_index] = float(np.exp(log_likelihood)) else: - det_ratio = 0.0 - log_term = np.sum(obs[:, time_index] * np.log(np.clip(lambda_delta.reshape(-1), 1e-12, np.inf)) - lambda_delta.reshape(-1)) - likelihoods[model_index] = float(det_ratio * np.exp(np.clip(log_term, -200.0, 50.0))) + likelihoods[model_index] = 0.0 finite_likelihoods = likelihoods.copy() finite_likelihoods[~np.isfinite(finite_likelihoods)] = 0.0 diff --git a/nstat/paper_gallery.py b/nstat/paper_gallery.py index 5e262960..ecd9c7ed 100644 --- a/nstat/paper_gallery.py +++ b/nstat/paper_gallery.py @@ -112,9 +112,7 @@ def render_paper_examples_markdown(repo_root: Path | None = None) -> str: def render_readme_examples_markdown(repo_root: Path | None = None) -> str: manifest = build_gallery_manifest(repo_root) lines = [ - "## Examples", - "", - "### Paper Examples (Self-Contained)", + "## Paper Examples (Self-Contained)", "", "Canonical source files:", "- `examples/paper/*.py`", @@ -146,16 +144,6 @@ def render_readme_examples_markdown(repo_root: Path | None = None) -> str: "Expanded paper-example index and figure gallery:", "- [docs/paper_examples.md](docs/paper_examples.md)", "", - "### Supplementary Examples", - "", - "These smaller demos remain useful as quick install and plotting checks.", - "", - "| Example | Run command | Output |", - "|---|---|---|", - "| Multitaper spectrum + spectrogram | `python examples/readme_examples/example1_multitaper_and_spectrogram.py` | [PNG](examples/readme_examples/images/readme_example1_multitaper_and_spectrogram.png) |", - "| Simulated CIF spike train | `python examples/readme_examples/example2_simulate_cif_spiketrain_10s.py` | [PNG](examples/readme_examples/images/readme_example2_simulate_cif_spiketrain_10s.png) |", - "| Spike-train raster | `python examples/readme_examples/example3_nstcoll_raster_from_example2.py` | [PNG](examples/readme_examples/images/readme_example3_nstcoll_raster.png) |", - "", ] ) return "\n".join(lines) @@ -194,10 +182,11 @@ def write_gallery_outputs(repo_root: Path | None = None) -> tuple[Path, Path, Pa readme_path = base / "README.md" readme_text = readme_path.read_text(encoding="utf-8") - start = readme_text.index("## Examples") - end = readme_text.index("## Documentation") + start = readme_text.index("## Paper Examples (Self-Contained)") + end = readme_text.index("Plot style policy:") + gallery_text = render_readme_examples_markdown(base) readme_path.write_text( - readme_text[:start] + render_readme_examples_markdown(base) + readme_text[end:], + readme_text[:start] + gallery_text + "\n" + readme_text[end:], encoding="utf-8", ) return manifest_path, markdown_path, readme_path diff --git a/parity/notebook_fidelity.yml b/parity/notebook_fidelity.yml index 2aa9a945..37ca7af3 100644 --- a/parity/notebook_fidelity.yml +++ b/parity/notebook_fidelity.yml @@ -1,5 +1,5 @@ version: 1 -generated_on: '2026-03-22' +generated_on: '2026-03-14' source_repositories: matlab: https://github.com/cajigaslab/nSTAT python: https://github.com/cajigaslab/nSTAT-python diff --git a/tests/test_docs_surface.py b/tests/test_docs_surface.py index 3a1ab1a7..2641846a 100644 --- a/tests/test_docs_surface.py +++ b/tests/test_docs_surface.py @@ -37,10 +37,16 @@ def test_matlab_style_navigation_pages_exist() -> None: assert path.exists(), f"Missing docs page: {path}" -def test_readme_links_to_help_navigation_pages() -> None: +def test_readme_links_to_github_pages_site() -> None: + """README links to the GitHub Pages site (mirrors MATLAB README structure).""" text = README_PATH.read_text(encoding="utf-8") - for page in MATLAB_NAV_PAGES: - assert f"[docs/{page}.md](docs/{page}.md)" in text + assert "cajigaslab.github.io/nSTAT-python" in text + + +def test_readme_links_to_paper_overview() -> None: + """README links to PaperOverview from the toolbox map section.""" + text = README_PATH.read_text(encoding="utf-8") + assert "[docs/PaperOverview.md](docs/PaperOverview.md)" in text def test_docs_conf_enables_markdown_support() -> None: diff --git a/tests/test_network_tutorial_builder.py b/tests/test_network_tutorial_builder.py index 275a6d40..5e1c1af6 100644 --- a/tests/test_network_tutorial_builder.py +++ b/tests/test_network_tutorial_builder.py @@ -16,6 +16,10 @@ def _normalize_notebook(notebook) -> None: cell["id"] = "normalized" cell["execution_count"] = None cell["outputs"] = [] + # Strip execution-related cell metadata added by nbconvert + cell.get("metadata", {}).pop("execution", None) + # Strip kernel-specific metadata that changes after execution + notebook.metadata.pop("language_info", None) def _cell_payload(cell) -> tuple[str, str, dict]: diff --git a/tests/test_paper_gallery_docs.py b/tests/test_paper_gallery_docs.py index 838bbee3..5a6bb823 100644 --- a/tests/test_paper_gallery_docs.py +++ b/tests/test_paper_gallery_docs.py @@ -24,9 +24,9 @@ def test_paper_examples_markdown_matches_generator() -> None: def test_readme_examples_block_matches_generator() -> None: committed = (REPO_ROOT / "README.md").read_text(encoding="utf-8") - start = committed.index("## Examples") - end = committed.index("## Documentation") - assert committed[start:end] == render_readme_examples_markdown(REPO_ROOT) + start = committed.index("## Paper Examples (Self-Contained)") + end = committed.index("\nPlot style policy:") + assert committed[start:end].rstrip() == render_readme_examples_markdown(REPO_ROOT).rstrip() def test_gallery_manifest_tracks_thumbnail_and_run_command_fields() -> None: diff --git a/tests/test_readme_examples_catalog.py b/tests/test_readme_examples_catalog.py index d463ec52..a486ef19 100644 --- a/tests/test_readme_examples_catalog.py +++ b/tests/test_readme_examples_catalog.py @@ -10,21 +10,6 @@ README_PATH = REPO_ROOT / "README.md" PAPER_MANIFEST_PATH = REPO_ROOT / "examples" / "paper" / "manifest.yml" -SUPPLEMENTARY_EXAMPLES = [ - ( - "python examples/readme_examples/example1_multitaper_and_spectrogram.py", - "[PNG](examples/readme_examples/images/readme_example1_multitaper_and_spectrogram.png)", - ), - ( - "python examples/readme_examples/example2_simulate_cif_spiketrain_10s.py", - "[PNG](examples/readme_examples/images/readme_example2_simulate_cif_spiketrain_10s.png)", - ), - ( - "python examples/readme_examples/example3_nstcoll_raster_from_example2.py", - "[PNG](examples/readme_examples/images/readme_example3_nstcoll_raster.png)", - ), -] - EXPECTED_CANONICAL_QUESTIONS = { "example01_mepsc_poisson": "Do mEPSCs follow constant vs piecewise Poisson firing under Mg2+ washout?", "example02_whisker_stimulus_thalamus": "How do explicit whisker stimulus and spike history improve thalamic GLM fits?", @@ -35,19 +20,22 @@ def _extract_examples_block(text: str) -> str: - match = re.search(r"## Examples\n(.*?)\n## Documentation\n", text, flags=re.S) + """Extract the Paper Examples section from README (MATLAB-aligned structure).""" + match = re.search( + r"## Paper Examples \(Self-Contained\)\n(.*?)\nPlot style policy:", + text, + flags=re.S, + ) if not match: - raise AssertionError("README is missing an Examples block bounded by '## Examples' and '## Documentation'.") + raise AssertionError( + "README is missing a Paper Examples block bounded by " + "'## Paper Examples (Self-Contained)' and 'Plot style policy:'." + ) return match.group(1) def test_readme_examples_headings_match_gallery_layout() -> None: block = _extract_examples_block(README_PATH.read_text(encoding="utf-8")) - headings = re.findall(r"^###\s+.+$", block, flags=re.M) - assert headings == [ - "### Paper Examples (Self-Contained)", - "### Supplementary Examples", - ] assert "python tools/paper_examples/build_gallery.py" in block @@ -75,15 +63,3 @@ def test_paper_example_manifest_questions_match_matlab_gallery_wording() -> None entries = manifest.get("examples", []) questions = {str(row["name"]): str(row["question"]) for row in entries} assert questions == EXPECTED_CANONICAL_QUESTIONS - - -def test_readme_supplementary_examples_are_preserved() -> None: - block = _extract_examples_block(README_PATH.read_text(encoding="utf-8")) - - positions: list[int] = [] - for command, png_link in SUPPLEMENTARY_EXAMPLES: - pos = block.find(command) - assert pos >= 0, f"Missing supplementary example command: {command}" - positions.append(pos) - assert png_link in block - assert positions == sorted(positions), "Supplementary examples must remain in README order." diff --git a/tests/test_readme_nstatpaperexamples.py b/tests/test_readme_nstatpaperexamples.py index a76e88d1..c95483ae 100644 --- a/tests/test_readme_nstatpaperexamples.py +++ b/tests/test_readme_nstatpaperexamples.py @@ -10,7 +10,11 @@ def test_readme_links_to_generated_paper_gallery_and_figure_dirs() -> None: text = README_PATH.read_text(encoding="utf-8") - match = re.search(r"## Examples\n(.*?)\n## Documentation\n", text, flags=re.S) + match = re.search( + r"## Paper Examples \(Self-Contained\)\n(.*?)\nPlot style policy:", + text, + flags=re.S, + ) assert match, "README is missing the paper examples block." block = match.group(1)