diff --git a/.github/workflows/notebook-full-fidelity.yml b/.github/workflows/notebook-full-fidelity.yml new file mode 100644 index 00000000..ac4cc6a2 --- /dev/null +++ b/.github/workflows/notebook-full-fidelity.yml @@ -0,0 +1,31 @@ +name: notebook-full-fidelity + +on: + pull_request: + paths: + - "notebooks/**" + - "tools/notebooks/**" + - "nstat/notebook_*.py" + - "parity/notebook_fidelity.yml" + - "parity/report.md" + schedule: + - cron: "15 7 * * 6" + workflow_dispatch: + +jobs: + helpfile-full: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e .[dev] + python -m pip install ipykernel + python -m ipykernel install --user --name python3 --display-name "Python 3" + - name: Execute mapped MATLAB helpfile notebooks + run: python tools/notebooks/run_notebooks.py --group helpfile_full --timeout 1200 diff --git a/notebooks/AnalysisExamples.ipynb b/notebooks/AnalysisExamples.ipynb index 1c90b5bb..b4181cf4 100644 --- a/notebooks/AnalysisExamples.ipynb +++ b/notebooks/AnalysisExamples.ipynb @@ -2,14 +2,14 @@ "cells": [ { "cell_type": "markdown", - "id": "db89e36d", + "id": "0f7ae1f5", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `AnalysisExamples.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: Advanced MATLAB algorithm-selection branches and some report plots are still lighter in Python." + "- Fidelity status: `partial`\n", + "- Remaining justified differences: Advanced MATLAB algorithm-selection branches and report plots remain lighter in Python, and the notebook still contains tracker-only visualization sections rather than a fully executable MATLAB-equivalent workflow." ] }, { diff --git a/notebooks/DecodingExample.ipynb b/notebooks/DecodingExample.ipynb index ae8a6a8d..781daba8 100644 --- a/notebooks/DecodingExample.ipynb +++ b/notebooks/DecodingExample.ipynb @@ -2,14 +2,14 @@ "cells": [ { "cell_type": "markdown", - "id": "72ddd907", + "id": "e78ea1c1", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `DecodingExample.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: Workflow, model fitting, and decoded-stimulus figures now follow the MATLAB helpfile closely; exact traces still depend on stochastic simulation draws and Python plotting defaults.\n" + "- Remaining justified differences: Workflow, model fitting, and decoded-stimulus figures now follow the MATLAB helpfile closely; exact traces still depend on stochastic simulation draws and Python plotting defaults." ] }, { @@ -215,4 +215,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/DecodingExampleWithHist.ipynb b/notebooks/DecodingExampleWithHist.ipynb index 302e98fb..2fd02734 100644 --- a/notebooks/DecodingExampleWithHist.ipynb +++ b/notebooks/DecodingExampleWithHist.ipynb @@ -2,14 +2,14 @@ "cells": [ { "cell_type": "markdown", - "id": "f624a68c", + "id": "04553c3e", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `DecodingExampleWithHist.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now mirrors the MATLAB history-aware decoding workflow closely; exact stochastic trajectories and figure styling still vary slightly under Python execution.\n" + "- Remaining justified differences: The notebook now mirrors the MATLAB history-aware decoding workflow closely; exact stochastic trajectories and figure styling still vary slightly under Python execution." ] }, { @@ -174,4 +174,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/ExplicitStimulusWhiskerData.ipynb b/notebooks/ExplicitStimulusWhiskerData.ipynb index 2e2a9117..7938e8f9 100644 --- a/notebooks/ExplicitStimulusWhiskerData.ipynb +++ b/notebooks/ExplicitStimulusWhiskerData.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "d90d2497", + "id": "1978cb81", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `ExplicitStimulusWhiskerData.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now reproduces the dataset-backed lag search, stimulus-effect, and history-effect workflow with real figures; exact KS traces and coefficient values still vary modestly from MATLAB because the Python GLM backend and plotting defaults are different.\n" + "- Remaining justified differences: The notebook now reproduces the dataset-backed lag search, stimulus-effect, and history-effect workflow with real figures; exact KS traces and coefficient values still vary modestly from MATLAB because the Python GLM backend and plotting defaults are different." ] }, { "cell_type": "code", "execution_count": null, - "id": "116b9deb", + "id": "9787cf60", "metadata": {}, "outputs": [], "source": [ @@ -83,7 +83,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30d8a376", + "id": "a60a7a6d", "metadata": {}, "outputs": [], "source": [ @@ -106,7 +106,7 @@ { "cell_type": "code", "execution_count": null, - "id": "72f337ee", + "id": "862db342", "metadata": {}, "outputs": [], "source": [ @@ -134,7 +134,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4b14768a", + "id": "98d686d5", "metadata": {}, "outputs": [], "source": [ @@ -149,7 +149,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c3e25d63", + "id": "2b5d9da0", "metadata": {}, "outputs": [], "source": [ @@ -170,7 +170,7 @@ { "cell_type": "code", "execution_count": null, - "id": "d1a12c7a", + "id": "4a033d5c", "metadata": {}, "outputs": [], "source": [ @@ -198,7 +198,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ebff91c2", + "id": "0a73d5e1", "metadata": {}, "outputs": [], "source": [ @@ -230,7 +230,7 @@ { "cell_type": "code", "execution_count": null, - "id": "368647ec", + "id": "5aa18805", "metadata": {}, "outputs": [], "source": [ @@ -290,4 +290,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/HippocampalPlaceCellExample.ipynb b/notebooks/HippocampalPlaceCellExample.ipynb index c4c230c2..6a81718a 100644 --- a/notebooks/HippocampalPlaceCellExample.ipynb +++ b/notebooks/HippocampalPlaceCellExample.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "0ec92178", + "id": "e0aeece6", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `HippocampalPlaceCellExample.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now reproduces the dataset-backed place-cell model-comparison and field-visualization workflow with real figures; the Python port still uses an approximate Zernike-like basis rather than the original MATLAB toolbox implementation.\n" + "- Remaining justified differences: The notebook now reproduces the dataset-backed place-cell model-comparison and field-visualization workflow with real figures; the Python port still uses an approximate Zernike-like basis rather than the original MATLAB toolbox implementation." ] }, { "cell_type": "code", "execution_count": null, - "id": "ad01cf3d", + "id": "cadf7961", "metadata": {}, "outputs": [], "source": [ @@ -85,7 +85,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5354bf29", + "id": "8c9b854e", "metadata": {}, "outputs": [], "source": [ @@ -105,7 +105,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24af9e4c", + "id": "bca6b3c3", "metadata": {}, "outputs": [], "source": [ @@ -125,7 +125,7 @@ { "cell_type": "code", "execution_count": null, - "id": "bff7c68f", + "id": "7ad76d69", "metadata": {}, "outputs": [], "source": [ @@ -152,7 +152,7 @@ { "cell_type": "code", "execution_count": null, - "id": "03f1d8f7", + "id": "516eb14e", "metadata": {}, "outputs": [], "source": [ @@ -179,7 +179,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a190b5ab", + "id": "711a2d08", "metadata": {}, "outputs": [], "source": [ @@ -270,4 +270,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/HybridFilterExample.ipynb b/notebooks/HybridFilterExample.ipynb index 0a794dfb..f3293504 100644 --- a/notebooks/HybridFilterExample.ipynb +++ b/notebooks/HybridFilterExample.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "dcd3b4c5", + "id": "deae92f2", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `HybridFilterExample.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now reproduces the hybrid-filter simulation, single-run decoding, and averaged summary figures with real outputs; the Python port still uses the current hybrid-filter implementation instead of every MATLAB-specific reporting branch.\n" + "- Remaining justified differences: The notebook now reproduces the hybrid-filter simulation, single-run decoding, and averaged summary figures with real outputs; the Python port still uses the current hybrid-filter implementation instead of every MATLAB-specific reporting branch." ] }, { "cell_type": "code", "execution_count": null, - "id": "e71ac4b2", + "id": "ad07c855", "metadata": {}, "outputs": [], "source": [ @@ -63,7 +63,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3990bad9", + "id": "8ad6c4c4", "metadata": {}, "outputs": [], "source": [ @@ -87,31 +87,29 @@ { "cell_type": "code", "execution_count": null, - "id": "f7752054", + "id": "d3676bdb", "metadata": {}, "outputs": [], "source": [ "# SECTION 1: Problem Statement\n", - "# We infer both a discrete movement state and a continuous reach trajectory from point-process observations.\n", - "pass\n" + "# We infer both a discrete movement state and a continuous reach trajectory from point-process observations.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "b271b968", + "id": "c0ac4ff8", "metadata": {}, "outputs": [], "source": [ "# SECTION 2: Hybrid state-space setup\n", - "# The Python port keeps the same two-state problem structure as MATLAB: a low-motion state and a movement state.\n", - "pass\n" + "# The Python port keeps the same two-state problem structure as MATLAB: a low-motion state and a movement state.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "e4d6c294", + "id": "20954cd6", "metadata": {}, "outputs": [], "source": [ @@ -163,19 +161,18 @@ { "cell_type": "code", "execution_count": null, - "id": "ffd10a0a", + "id": "e59c36e8", "metadata": {}, "outputs": [], "source": [ "# SECTION 4: Simulate Neural Firing\n", - "# The simulated spike population depends on the latent state and the movement dynamics.\n", - "pass\n" + "# The simulated spike population depends on the latent state and the movement dynamics.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "db1960aa", + "id": "1da0a680", "metadata": {}, "outputs": [], "source": [ @@ -251,4 +248,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/PPSimExample.ipynb b/notebooks/PPSimExample.ipynb index 4dcb3554..73497238 100644 --- a/notebooks/PPSimExample.ipynb +++ b/notebooks/PPSimExample.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "8968c9f0", + "id": "f5cccb2d", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `PPSimExample.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: MATLAB plotting/report formatting remains lighter, but the core point-process simulation workflow is closely aligned." + "- Fidelity status: `partial`\n", + "- Remaining justified differences: The notebook now executes the full Python point-process simulation and analysis workflow without placeholders, but it still uses the native `CIFModel` path rather than the original MATLAB/Simulink recursive CIF model." ] }, { "cell_type": "code", "execution_count": null, - "id": "ecca9124", + "id": "17854318", "metadata": {}, "outputs": [], "source": [ @@ -34,147 +34,160 @@ "matplotlib.use(\"Agg\")\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from scipy.io import loadmat\n", "\n", - "from nstat.data_manager import ensure_example_data\n", + "from nstat import Analysis, CIFModel, ConfigColl, CovColl, Covariate, FitResSummary, Trial, TrialConfig\n", "from nstat.notebook_figures import FigureTracker\n", "\n", - "np.random.seed(0)\n", - "DATA_DIR = ensure_example_data(download=True)\n", + "np.random.seed(5)\n", "OUTPUT_ROOT = REPO_ROOT / \"output\" / \"notebook_images\"\n", "__tracker = FigureTracker(topic='PPSimExample', output_root=OUTPUT_ROOT, expected_count=3)\n", "\n", - "def _load_example_globals(name: str) -> dict[str, object]:\n", - " candidates = [\n", - " Path(name),\n", - " DATA_DIR / name,\n", - " DATA_DIR / \"mEPSCs\" / name,\n", - " DATA_DIR / \"Place Cells\" / name,\n", - " DATA_DIR / \"Explicit Stimulus\" / name,\n", - " ]\n", - " for path in candidates:\n", - " if path.exists():\n", - " data = loadmat(path)\n", - " return {k: v for k, v in data.items() if not k.startswith(\"__\")}\n", - " return {}\n", "\n", - "# SECTION 0: Section 0\n", - "# General Point Process Simulation\n", - "# In this demo, we show how sample-paths of a point process (PP) can be generated from specification of its conditional intensity function (CIF). We then use the generated PP data to validate the outputs of the Neural Spike Analysis Toolbox." + "def _figure(label: str, *, figsize=(8.5, 4.5)):\n", + " fig = __tracker.new_figure(label)\n", + " fig.clear()\n", + " fig.set_size_inches(*figsize)\n", + " return fig\n", + "\n", + "\n", + "def _logistic_rate(time, stimulus, mu=-3.0):\n", + " dt = float(np.median(np.diff(time)))\n", + " eta = mu + stimulus\n", + " p = np.exp(np.clip(eta, -20.0, 20.0))\n", + " p = p / (1.0 + p)\n", + " return p / max(dt, 1e-12)\n", + "\n", + "\n", + "Ts = 0.001\n", + "tMin = 0.0\n", + "tMax = 10.0\n", + "t = np.arange(tMin, tMax + Ts, Ts)\n", + "mu = -3.0\n", + "stimulus_signal = np.sin(2 * np.pi * 1.0 * t)\n", + "baseline = Covariate(t, np.ones_like(t), \"Baseline\", \"time\", \"s\", \"\", [\"mu\"])\n", + "stim = Covariate(t, stimulus_signal, \"Stimulus\", \"time\", \"s\", \"Voltage\", [\"sin\"])\n", + "rate_hz = _logistic_rate(t, stimulus_signal, mu=mu)\n", + "lambda_model = CIFModel(t, rate_hz, name=\"lambda\")\n", + "sC = lambda_model.simulate(num_realizations=5, seed=5)\n", + "cc = CovColl([stim, baseline])\n", + "trial = Trial(sC, cc)\n", + "print({\"duration_s\": tMax, \"num_realizations\": sC.numSpikeTrains, \"mean_rate_hz\": round(float(np.mean(rate_hz)), 3)})\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40cc9e1d", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 1: General Point Process Simulation\n", + "plt.close(\"all\")\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "87281c9b", + "id": "c9a47fd4", "metadata": {}, "outputs": [], "source": [ - "# SECTION 1: Point Process Sample Path Generation\n", - "# That both the stimulus effect and ensemble effects can be made into multi-input/multi-output transfer functions to account for more than 1 stimulus effect or multiple neighboring neuron effects. To do this, simply define E$ orS$ to be a row vector of LTI transfer functions. Make sure than the number of dimensions of the input matches the number of transfer functions specified in the row vector.\n", - "# This block diagram specifies a conditional intensity function of the form\n", - "# \\lambda_{i} \\cdot \\Delta = exp(\\mu_{i} + H*\\Delta N_{i}[n] + S*u_{stim}[n] + E*\\Delta N_{k}[n])/(1+exp(\\mu_{i} + H*\\Delta N_{i}[n] + S*u_{stim}[n] + E*\\Delta N_{k}[n]))\n", - "plt.close(\"all\")\n", - "Ts = .001\n", - "#\n", - "mu = -3" + "# SECTION 2: Point Process Sample Path Generation\n", + "# This Python port uses a native CIFModel-driven rate simulation instead of the original MATLAB/Simulink model.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "85f598f6", + "id": "8c3d2ff7", "metadata": {}, "outputs": [], "source": [ - "# SECTION 2: History Effect\n", - "# 1*h[n]=-1*\\Delta N[n-1]-2*\\Delta N[n-2] -4*\\Delta N[n-3]" + "# SECTION 3: History Effect\n", + "selfHist = [0.0, 0.001, 0.002, 0.003]\n", + "print({\"history_windows_s\": selfHist})\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "3c347f52", + "id": "18b68a07", "metadata": {}, "outputs": [], "source": [ - "# SECTION 3: Stimulus Effect\n", - "# 1*s[n]=1*u_{stim}[n]" + "# SECTION 4: Stimulus Effect\n", + "print({\"stimulus_frequency_hz\": 1.0, \"stimulus_amplitude\": 1.0})\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "c56ac644", + "id": "00dd650a", "metadata": {}, "outputs": [], "source": [ - "# SECTION 4: Ensemble Effect\n", - "# 1*e[n]=0*\\Delta N_{k}[n]\n", - "f = 1\n", - "#\n", - "numRealizations = 5\n", - "__tracker.new_figure('figure')\n", - "__tracker.new_figure('figure;')\n", - "__tracker.annotate('subplot(2,1,1)')\n", - "__tracker.annotate('sC.plot')\n", - "__tracker.annotate('subplot(2,1,2)')\n", - "__tracker.annotate('stim.plot')" + "# SECTION 5: Ensemble Effect\n", + "print({\"ensemble_effect\": 0.0})\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "6fee4d96", + "id": "730efaf0", "metadata": {}, "outputs": [], "source": [ - "# SECTION 5: GLM Model Fitting Setup\n", - "# In this section, we create the appropriate structures to fit several GLM models to the data generated above.\n", - "# Create a constant covariate representing the mean firing rate $$\\mu_{i}$\n", - "#\n", - "#" + "# 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", + "axs = fig.subplots(2, 1, sharex=True)\n", + "sC.plot(handle=axs[0])\n", + "axs[0].set_xlim(0.0, tMax / 5.0)\n", + "stim.plot(handle=axs[1])\n", + "axs[1].set_xlim(0.0, tMax / 5.0)\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "c7e17a6c", + "id": "6dead7c9", "metadata": {}, "outputs": [], "source": [ - "# SECTION 6: GLM Model Fitting and Results\n", - "pass\n", - "# Fit only a mean firing rate\n", - "# Fit a mean firing rate + the stimulus term\n", - "# Fit a mean firing rate, self-history, and stimulus --- Same as true model\n", - "__tracker.annotate(\"c{3}.setName('Stim+Hist')\")\n", - "# Place all configurations together and run analysis for each neuron\n", - "# Binomial Logistic Regression with Conjugate\n", - "# Gradient Solver by Demba Ba (demba@mit.edu).\n", - "# or Poisson CIFs" + "# SECTION 7: GLM Model Fitting Setup\n", + "cfg = [\n", + " TrialConfig([[\"Baseline\", \"mu\"]], sampleRate=1.0 / Ts, name=\"Baseline\"),\n", + " TrialConfig([[\"Baseline\", \"mu\"], [\"Stimulus\", \"sin\"]], sampleRate=1.0 / Ts, name=\"Stim\"),\n", + " TrialConfig([[\"Baseline\", \"mu\"], [\"Stimulus\", \"sin\"]], sampleRate=1.0 / Ts, history=selfHist, name=\"Stim+Hist\"),\n", + "]\n", + "cfgColl = ConfigColl(cfg)\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "9793b981", + "id": "423d29ba", "metadata": {}, "outputs": [], "source": [ - "# SECTION 7: Results for sample neuron" + "# SECTION 8: GLM Model Fitting and Results\n", + "results = Analysis.RunAnalysisForAllNeurons(trial, cfgColl)\n", + "fig = _figure(\"results{1}.plotResults\", figsize=(11.0, 8.0))\n", + "results[0].plotResults(handle=fig)\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "f0e1dff8", + "id": "eea9ca02", "metadata": {}, "outputs": [], "source": [ - "# SECTION 8: Results for across all sample paths\n", - "__tracker.annotate('Summary.plotSummary')\n", - "__tracker.finalize()" + "# SECTION 9: Results for across all sample paths\n", + "summary = FitResSummary(results)\n", + "fig = _figure(\"Summary.plotSummary\", figsize=(10.0, 4.5))\n", + "summary.plotSummary(handle=fig)\n", + "print({\"fit_names\": summary.fitNames, \"mean_AIC\": np.asarray(summary.AIC, dtype=float).round(3).tolist()})\n", + "__tracker.finalize()\n" ] } ], diff --git a/notebooks/StimulusDecode2D.ipynb b/notebooks/StimulusDecode2D.ipynb index 8b8d1f88..469531c6 100644 --- a/notebooks/StimulusDecode2D.ipynb +++ b/notebooks/StimulusDecode2D.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "34a2384f", + "id": "832f42b3", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `StimulusDecode2D.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now reproduces the 2-D stimulus-decoding workflow with simulated receptive fields and decoded trajectories; the current Python decoder uses regression-based state recovery instead of MATLAB's symbolic-CIF nonlinear filter.\n" + "- Remaining justified differences: The notebook now reproduces the 2-D stimulus-decoding workflow with simulated receptive fields and decoded trajectories; the current Python decoder uses regression-based state recovery instead of MATLAB's symbolic-CIF nonlinear filter." ] }, { "cell_type": "code", "execution_count": null, - "id": "98f026d3", + "id": "610a2d16", "metadata": {}, "outputs": [], "source": [ @@ -122,7 +122,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4a0a0f39", + "id": "7b19bc85", "metadata": {}, "outputs": [], "source": [ @@ -136,7 +136,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fc9cbb7d", + "id": "c4494ced", "metadata": {}, "outputs": [], "source": [ @@ -178,7 +178,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7d176229", + "id": "7e531f49", "metadata": {}, "outputs": [], "source": [ @@ -196,7 +196,7 @@ { "cell_type": "code", "execution_count": null, - "id": "985e121e", + "id": "435982bd", "metadata": {}, "outputs": [], "source": [ @@ -252,4 +252,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/TrialExamples.ipynb b/notebooks/TrialExamples.ipynb index ccdf0c95..89fb8d24 100644 --- a/notebooks/TrialExamples.ipynb +++ b/notebooks/TrialExamples.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "93f72e2f", + "id": "854c1e8d", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `TrialExamples.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: Some MATLAB plotting/display details remain simplified, but the core Trial object workflow now follows the MATLAB semantics closely." + "- Remaining justified differences: The notebook now mirrors the MATLAB Trial workflow with executable object construction, masking, history extraction, and plotting; the closing analysis section uses one representative Python `Analysis` run instead of linking out to separate MATLAB help pages." ] }, { "cell_type": "code", "execution_count": null, - "id": "2e24c2d5", + "id": "d8ec4875", "metadata": {}, "outputs": [], "source": [ @@ -34,76 +34,220 @@ "matplotlib.use(\"Agg\")\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from scipy.io import loadmat\n", "\n", - "from nstat.data_manager import ensure_example_data\n", + "from nstat import Analysis, ConfigColl, CovColl, Covariate, Events, History, Trial, TrialConfig, nspikeTrain, nstColl\n", "from nstat.notebook_figures import FigureTracker\n", "\n", - "np.random.seed(0)\n", - "DATA_DIR = ensure_example_data(download=True)\n", + "np.random.seed(7)\n", "OUTPUT_ROOT = REPO_ROOT / \"output\" / \"notebook_images\"\n", "__tracker = FigureTracker(topic='TrialExamples', output_root=OUTPUT_ROOT, expected_count=6)\n", "\n", - "def _load_example_globals(name: str) -> dict[str, object]:\n", - " candidates = [\n", - " Path(name),\n", - " DATA_DIR / name,\n", - " DATA_DIR / \"mEPSCs\" / name,\n", - " DATA_DIR / \"Place Cells\" / name,\n", - " DATA_DIR / \"Explicit Stimulus\" / name,\n", - " ]\n", - " for path in candidates:\n", - " if path.exists():\n", - " data = loadmat(path)\n", - " return {k: v for k, v in data.items() if not k.startswith(\"__\")}\n", - " return {}\n", "\n", - "# SECTION 0: Section 0\n", - "# Trial Examples" + "def _figure(label: str, *, figsize=(8.5, 3.5)):\n", + " fig = __tracker.new_figure(label)\n", + " fig.clear()\n", + " fig.set_size_inches(*figsize)\n", + " return fig\n", + "\n", + "\n", + "def _build_trial():\n", + " length_trial = 1.0\n", + " sample_rate = 1000.0\n", + " time = np.linspace(0.0, length_trial, int(length_trial * sample_rate) + 1)\n", + "\n", + " position = Covariate(\n", + " time,\n", + " np.column_stack([np.sin(2 * np.pi * time), np.cos(2 * np.pi * time)]),\n", + " \"Position\",\n", + " \"time\",\n", + " \"s\",\n", + " \"a.u.\",\n", + " [\"x\", \"y\"],\n", + " )\n", + " force = Covariate(\n", + " time,\n", + " np.column_stack(\n", + " [\n", + " 0.5 * np.cos(4 * np.pi * time) + 0.15 * np.sin(2 * np.pi * time),\n", + " 0.4 * np.sin(4 * np.pi * time + 0.3),\n", + " ]\n", + " ),\n", + " \"Force\",\n", + " \"time\",\n", + " \"s\",\n", + " \"N\",\n", + " [\"f_x\", \"f_y\"],\n", + " )\n", + " cov_coll = CovColl([position, force])\n", + " cov_coll.setMaxTime(length_trial)\n", + "\n", + " events = Events([0.18, 0.72], [\"E_1\", \"E_2\"])\n", + " history = History([0.0, 0.1, 0.2, 0.4])\n", + "\n", + " trains = []\n", + " base_grid = np.linspace(0.05, 0.95, 100)\n", + " for neuron_index, phase in enumerate(np.linspace(0.0, np.pi / 2.0, 4), start=1):\n", + " spikes = np.clip(base_grid + 0.008 * np.sin(2 * np.pi * base_grid * (neuron_index + 1) + phase), 0.0, length_trial)\n", + " trains.append(\n", + " nspikeTrain(\n", + " np.sort(spikes),\n", + " name=str(neuron_index),\n", + " minTime=0.0,\n", + " maxTime=length_trial,\n", + " makePlots=-1,\n", + " )\n", + " )\n", + " spike_coll = nstColl(trains)\n", + " trial = Trial(spike_coll, cov_coll, events, history)\n", + " return {\n", + " \"length_trial\": length_trial,\n", + " \"sample_rate\": sample_rate,\n", + " \"history\": history,\n", + " \"cov_coll\": cov_coll,\n", + " \"events\": events,\n", + " \"spike_coll\": spike_coll,\n", + " \"trial\": trial,\n", + " }\n", + "\n", + "\n", + "ctx = _build_trial()\n", + "print(\n", + " {\n", + " \"trial_duration_s\": ctx[\"length_trial\"],\n", + " \"num_neurons\": ctx[\"spike_coll\"].numSpikeTrains,\n", + " \"covariates\": ctx[\"cov_coll\"].names,\n", + " \"history_windows\": ctx[\"history\"].windowTimes.tolist(),\n", + " }\n", + ")\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "a84ec88f", + "id": "383ce8dd", "metadata": {}, "outputs": [], "source": [ "# SECTION 1: Example 1: A simple data set\n", "plt.close(\"all\")\n", - "lengthTrial = 1\n", - "# Create History windows of interest\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('h.plot')\n", - "# Load Covariates\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('cc.plot')\n", - "# Create trial events\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('e.plot')\n", - "# Create neural Spike Train Data\n", - "pass\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('spikeColl.plot')\n", - "# Finally we have everything we need to create a Trial object.\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('trial1.plot')\n", - "# Mask out some of the data and plot the trial once again\n", - "__tracker.new_figure('figure')\n", - "__tracker.annotate('trial1.plot')\n", - "#" + "trial1 = ctx[\"trial\"]\n", + "spikeColl = ctx[\"spike_coll\"]\n", + "cc = ctx[\"cov_coll\"]\n", + "e = ctx[\"events\"]\n", + "h = ctx[\"history\"]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75e47953", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 2: Create History windows of interest\n", + "fig = _figure(\"figure; h.plot\", figsize=(8.0, 2.5))\n", + "ax = fig.subplots(1, 1)\n", + "h.plot(handle=ax)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af2d5662", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 3: Load Covariates\n", + "fig = _figure(\"figure; cc.plot\", figsize=(8.5, 5.0))\n", + "cc.plot(handle=fig)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43f4942f", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 4: Create trial events\n", + "fig = _figure(\"figure; e.plot\", figsize=(8.0, 2.3))\n", + "ax = fig.subplots(1, 1)\n", + "e.plot(handle=ax)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62b88bfc", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 5: Create neural Spike Train Data\n", + "fig = _figure(\"figure; spikeColl.plot\", figsize=(8.5, 3.5))\n", + "ax = fig.subplots(1, 1)\n", + "spikeColl.plot(handle=ax)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98cad713", + "metadata": {}, + "outputs": [], + "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", + "trial1.plot(handle=fig)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e40aaae7", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 7: Mask out some of the data and plot the trial once again\n", + "trial1.setCovMask([[\"Position\", \"x\"], [\"Force\", \"f_x\"]])\n", + "fig = _figure(\"figure; trial1.plot masked\", figsize=(9.0, 8.0))\n", + "trial1.plot(handle=fig)\n", + "hist_cov = trial1.getHistForNeurons([1, 2])\n", + "print({\"masked_labels\": trial1.getLabelsFromMask(1), \"history_covariates\": hist_cov.getAllCovLabels()[:4]})\n", + "trial1.resetCovMask()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e5b2603", + "metadata": {}, + "outputs": [], + "source": [ + "# SECTION 8: Example 2: Analyzing Trial Data\n", + "cfg = TrialConfig([[\"Position\", \"x\"], [\"Force\", \"f_x\"]], sampleRate=ctx[\"sample_rate\"], history=[0.0, 0.05, 0.1], name=\"Position+Force+History\")\n", + "cfgColl = ConfigColl([cfg])\n", + "fit = Analysis.RunAnalysisForNeuron(trial1, 1, cfgColl)\n", + "fit_stats = fit.computeKSStats()\n", + "print(\n", + " {\n", + " \"config_name\": fit.configNames[0],\n", + " \"aic\": round(float(fit.AIC[0]), 3),\n", + " \"bic\": round(float(fit.BIC[0]), 3),\n", + " \"ks_stat\": round(float(fit_stats[\"ks_stat\"]), 4),\n", + " }\n", + ")\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "a7f02ad1", + "id": "bf80910c", "metadata": {}, "outputs": [], "source": [ - "# SECTION 2: Example 2: Analyzing Trial Data\n", - "# Examples of neural spike analysis using the Neural Spike Analysis Toolbox or using standard methods standard methods\n", - "__tracker.finalize()" + "# SECTION 9: Related analysis workflows\n", + "print(\"For larger model-comparison walkthroughs, continue with AnalysisExamples and AnalysisExamples2.\")\n", + "__tracker.finalize()\n" ] } ], diff --git a/notebooks/ValidationDataSet.ipynb b/notebooks/ValidationDataSet.ipynb index c35a9cf5..3337a575 100644 --- a/notebooks/ValidationDataSet.ipynb +++ b/notebooks/ValidationDataSet.ipynb @@ -2,20 +2,20 @@ "cells": [ { "cell_type": "markdown", - "id": "f31e43b8", + "id": "a034081e", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `ValidationDataSet.mlx`\n", "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now reproduces the constant-rate and piecewise-rate validation workflows with real `Trial`/`Analysis` objects and figure outputs; the Python port uses shorter deterministic simulations than MATLAB so the notebook remains stable in CI.\n" + "- Remaining justified differences: The notebook now reproduces the constant-rate and piecewise-rate validation workflows with real `Trial`/`Analysis` objects and figure outputs; the Python port uses shorter deterministic simulations than MATLAB so the notebook remains stable in CI." ] }, { "cell_type": "code", "execution_count": null, - "id": "854fba02", + "id": "835e27fe", "metadata": {}, "outputs": [], "source": [ @@ -143,7 +143,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ab008b21", + "id": "31374daa", "metadata": {}, "outputs": [], "source": [ @@ -164,32 +164,30 @@ { "cell_type": "code", "execution_count": null, - "id": "042451f2", + "id": "f56503ae", "metadata": {}, "outputs": [], "source": [ "# SECTION 1: Case #1: Constant Rate Poisson Process\n", - "# First we verify that the analysis recovers a constant Poisson rate from simulated spike trains.\n", - "pass\n" + "# First we verify that the analysis recovers a constant Poisson rate from simulated spike trains.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "26629d03", + "id": "3c7479dd", "metadata": {}, "outputs": [], "source": [ "# SECTION 2: Generate constant-rate neural firing activity\n", "constant_time = np.asarray(constant_case[\"time_s\"], dtype=float)\n", - "constant_trains = list(constant_case[\"trains\"])\n", - "pass\n" + "constant_trains = list(constant_case[\"trains\"])\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "fceed463", + "id": "5c357cd0", "metadata": {}, "outputs": [], "source": [ @@ -203,7 +201,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61a469fd", + "id": "15efa1ec", "metadata": {}, "outputs": [], "source": [ @@ -225,7 +223,7 @@ { "cell_type": "code", "execution_count": null, - "id": "736796c6", + "id": "69d4e988", "metadata": {}, "outputs": [], "source": [ @@ -247,21 +245,20 @@ { "cell_type": "code", "execution_count": null, - "id": "33a56829", + "id": "ff7b1cfd", "metadata": {}, "outputs": [], "source": [ "# SECTION 6: Case #2: Piece-wise Constant Rate Poisson Process\n", "# Next we compare a single-rate model against a two-epoch rate model.\n", "piecewise_time = np.asarray(piecewise_case[\"time_s\"], dtype=float)\n", - "piecewise_trains = list(piecewise_case[\"trains\"])\n", - "pass\n" + "piecewise_trains = list(piecewise_case[\"trains\"])\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "4b10c04a", + "id": "d2e52928", "metadata": {}, "outputs": [], "source": [ @@ -291,19 +288,18 @@ { "cell_type": "code", "execution_count": null, - "id": "e7ef9b5f", + "id": "52b4802a", "metadata": {}, "outputs": [], "source": [ "# SECTION 8: Setup the piecewise-rate analysis\n", - "piecewise_results = Analysis.RunAnalysisForAllNeurons(piecewise_case[\"trial\"], piecewise_case[\"cfg\"], 0)\n", - "pass\n" + "piecewise_results = Analysis.RunAnalysisForAllNeurons(piecewise_case[\"trial\"], piecewise_case[\"cfg\"], 0)\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "b54dd710", + "id": "4119e5a0", "metadata": {}, "outputs": [], "source": [ @@ -338,7 +334,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f1d30942", + "id": "a0a82984", "metadata": {}, "outputs": [], "source": [ @@ -382,4 +378,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/notebooks/nSTATPaperExamples.ipynb b/notebooks/nSTATPaperExamples.ipynb index b08f0a69..f38860cb 100644 --- a/notebooks/nSTATPaperExamples.ipynb +++ b/notebooks/nSTATPaperExamples.ipynb @@ -2,14 +2,14 @@ "cells": [ { "cell_type": "markdown", - "id": "21e0c2ce", + "id": "98d25ffd", "metadata": {}, "source": [ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `nSTATPaperExamples.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: Python uses standalone figshare-backed data access and generated gallery assets rather than MATLAB path-based setup." + "- Fidelity status: `partial`\n", + "- Remaining justified differences: Python uses standalone figshare-backed data access and generated gallery assets rather than MATLAB path-based setup, and several sections still rely on placeholder or tracker-only cells instead of full MATLAB-equivalent computations." ] }, { diff --git a/nstat/events.py b/nstat/events.py index 9c306fd2..e99e8dd2 100644 --- a/nstat/events.py +++ b/nstat/events.py @@ -2,6 +2,10 @@ from typing import Any, Sequence +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt import numpy as np @@ -38,8 +42,19 @@ def fromStructure(structure: dict[str, Any] | None) -> "Events" | None: event_color = structure.get("eventColor", "r") return Events(event_times, event_labels, event_color) - def plot(self, *_, **__) -> None: - return None + def plot(self, *_, handle=None, **__): + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 2.2))[1] + ax.clear() + if self.eventTimes.size: + ax.vlines(self.eventTimes, 0.0, 1.0, color=self.eventColor, linewidth=1.5) + for x, label in zip(self.eventTimes, self.eventLabels, strict=False): + if label: + ax.text(float(x), 1.02, label, rotation=45, ha="left", va="bottom", fontsize=8) + ax.set_ylim(0.0, 1.1) + ax.set_xlabel("time [s]") + ax.set_yticks([]) + ax.set_title("Events") + return ax __all__ = ["Events"] diff --git a/nstat/fit.py b/nstat/fit.py index 73e9bf6d..78008457 100644 --- a/nstat/fit.py +++ b/nstat/fit.py @@ -3,7 +3,12 @@ from dataclasses import dataclass from typing import Any, Iterable, Sequence +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt import numpy as np +from scipy.stats import kstest from .core import Covariate, nspikeTrain @@ -36,6 +41,46 @@ def _pad_rows(rows: Sequence[np.ndarray], fill_value: float = np.nan) -> np.ndar return out +def _autocorrelation(values: np.ndarray, max_lag: int = 25) -> tuple[np.ndarray, np.ndarray]: + centered = np.asarray(values, dtype=float).reshape(-1) - float(np.mean(values)) + if centered.size < 2 or float(np.var(centered)) <= 0.0: + return np.asarray([], dtype=float), np.asarray([], dtype=float) + corr = np.correlate(centered, centered, mode="full") + corr = corr[corr.size // 2 :] + corr = corr / corr[0] + lags = np.arange(corr.shape[0], dtype=float) + max_lag = int(min(max_lag, corr.shape[0] - 1)) + return lags[1 : max_lag + 1], corr[1 : max_lag + 1] + + +def _time_rescaled_uniforms(y: np.ndarray, lam_per_bin: np.ndarray) -> np.ndarray: + y_arr = np.asarray(y, dtype=float).reshape(-1) + lam = np.asarray(lam_per_bin, dtype=float).reshape(-1) + if y_arr.shape != lam.shape: + raise ValueError("y and lam_per_bin must have the same shape") + if np.sum(y_arr) <= 1: + return np.asarray([], dtype=float) + + uniforms: list[float] = [] + accum = 0.0 + for count, lam_i in zip(y_arr, lam, strict=False): + accum += float(max(lam_i, 1e-12)) + if count >= 1.0: + for _ in range(int(round(count))): + uniforms.append(1.0 - np.exp(-accum)) + accum = 0.0 + return np.asarray(uniforms, dtype=float) + + +def _ks_curve(uniforms: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + u = np.sort(np.asarray(uniforms, dtype=float).reshape(-1)) + if u.size == 0: + return np.asarray([], dtype=float), np.asarray([], dtype=float), np.asarray([], dtype=float) + ideal = np.linspace(1.0 / u.size, 1.0, u.size, dtype=float) + ci = np.full(u.size, 1.36 / np.sqrt(float(u.size)), dtype=float) + return ideal, u, ci + + @dataclass class _SingleFit: name: str @@ -96,6 +141,7 @@ def _init_common(self) -> None: self.U = np.array([], dtype=float) self.X = np.array([], dtype=float) self.Residual = None + self._diagnostic_cache: dict[int, dict[str, np.ndarray | float]] = {} self.KSStats = np.zeros((self.numResults, 1), dtype=float) self.KSPvalues = np.full(self.numResults, np.nan, dtype=float) self.withinConfInt = np.zeros(self.numResults, dtype=float) @@ -296,23 +342,183 @@ def mergeResults(self, other: "FitResult") -> "FitResult": fits=[*self.fits, *other.fits], ) - def plotResults(self, *_, **__) -> None: - return None - - def KSPlot(self, *_, **__) -> None: - return None + def _lambda_series(self, fit_num: int = 1) -> tuple[np.ndarray, np.ndarray]: + time = np.asarray(self.lambda_signal.time, dtype=float).reshape(-1) + data = np.asarray(self.lambda_signal.data, dtype=float) + if data.ndim == 1: + rate = data.reshape(-1) + else: + idx = min(max(fit_num - 1, 0), data.shape[1] - 1) + rate = data[:, idx].reshape(-1) + if time.shape[0] != rate.shape[0]: + raise ValueError("lambda signal time and data lengths do not match") + return time, rate + + def _primary_spike_train(self) -> nspikeTrain: + if isinstance(self.neuralSpikeTrain, nspikeTrain): + return self.neuralSpikeTrain + if isinstance(self.neuralSpikeTrain, Sequence) and self.neuralSpikeTrain: + return self.neuralSpikeTrain[0] + raise TypeError("FitResult does not contain a MATLAB-style neural spike train") + + def _compute_diagnostics(self, fit_num: int = 1) -> dict[str, np.ndarray | float]: + if fit_num in self._diagnostic_cache: + return self._diagnostic_cache[fit_num] + + time, rate_hz = self._lambda_series(fit_num) + dt = float(np.median(np.diff(time))) if time.size > 1 else 1.0 + edges = np.concatenate([time, [time[-1] + dt]]) + counts = self._primary_spike_train().to_binned_counts(edges) + lam_per_bin = rate_hz * dt + residual = counts - lam_per_bin + residual_std = residual / np.sqrt(np.maximum(lam_per_bin, 1e-12)) + uniforms = _time_rescaled_uniforms(counts, lam_per_bin) + ideal, empirical, ci = _ks_curve(uniforms) + ks_stat = float(np.max(np.abs(empirical - ideal))) if ideal.size else 0.0 + ks_pvalue = float(kstest(uniforms, "uniform").pvalue) if uniforms.size else np.nan + within = float(np.mean(np.abs(empirical - ideal) <= ci)) if ideal.size else np.nan + lags, acf = _autocorrelation(uniforms, max_lag=25) + acf_ci = 1.96 / np.sqrt(float(uniforms.size)) if uniforms.size else np.nan + gauss = np.clip(uniforms, 1e-6, 1.0 - 1e-6) + coeffs = self.getCoeffs(fit_num) + coeff_labels = ["Intercept", *self.covLabels[fit_num - 1]] if fit_num - 1 < len(self.covLabels) else ["Intercept"] + diagnostics: dict[str, np.ndarray | float] = { + "time": time, + "rate_hz": rate_hz, + "counts": counts, + "lambda_per_bin": lam_per_bin, + "residual": residual, + "residual_std": residual_std, + "uniforms": uniforms, + "ks_ideal": ideal, + "ks_empirical": empirical, + "ks_ci": ci, + "ks_stat": ks_stat, + "ks_pvalue": ks_pvalue, + "within_conf_int": within, + "acf_lags": lags, + "acf_values": acf, + "acf_ci": acf_ci, + "gaussianized": gauss, + "coefficients": coeffs, + "coeff_labels": np.asarray(coeff_labels, dtype=object), + } + self._diagnostic_cache[fit_num] = diagnostics + self.KSStats[fit_num - 1, 0] = ks_stat + self.KSPvalues[fit_num - 1] = ks_pvalue + self.withinConfInt[fit_num - 1] = within + self.U = uniforms + self.Z = gauss + self.X = time + self.Residual = { + "time": time, + "residual": residual, + "standardized": residual_std, + } + self.invGausStats = {"rhoSig": acf.tolist(), "confBoundSig": [acf_ci]} + return diagnostics - def plotResidual(self, *_, **__) -> None: - return None + def computeKSStats(self, fit_num: int = 1) -> dict[str, float]: + diag = self._compute_diagnostics(fit_num) + return { + "ks_stat": float(diag["ks_stat"]), + "ks_pvalue": float(diag["ks_pvalue"]), + "within_conf_int": float(diag["within_conf_int"]), + } - def plotInvGausTrans(self, *_, **__) -> None: - return None + def computeInvGausTrans(self, fit_num: int = 1) -> np.ndarray: + return np.asarray(self._compute_diagnostics(fit_num)["uniforms"], dtype=float) - def plotSeqCorr(self, *_, **__) -> None: - return None + def computeFitResidual(self, fit_num: int = 1) -> Covariate: + diag = self._compute_diagnostics(fit_num) + return Covariate( + np.asarray(diag["time"], dtype=float), + np.asarray(diag["residual"], dtype=float), + "fit residual", + "time", + "s", + "counts/bin", + ["residual"], + ) - def plotCoeffs(self, *_, **__) -> None: - return None + def plotResults(self, fit_num: int = 1, handle=None): + fig = handle if handle is not None else plt.figure(figsize=(11.5, 8.0)) + fig.clear() + axes = fig.subplots(2, 2) + self.KSPlot(fit_num=fit_num, handle=axes[0, 0]) + self.plotInvGausTrans(fit_num=fit_num, handle=axes[0, 1]) + self.plotSeqCorr(fit_num=fit_num, handle=axes[1, 0]) + self.plotCoeffs(fit_num=fit_num, handle=axes[1, 1]) + fig.tight_layout() + return fig + + def KSPlot(self, fit_num: int = 1, handle=None): + diag = self._compute_diagnostics(fit_num) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(5.0, 4.0))[1] + ideal = np.asarray(diag["ks_ideal"], dtype=float) + empirical = np.asarray(diag["ks_empirical"], dtype=float) + ci = np.asarray(diag["ks_ci"], dtype=float) + if ideal.size: + ax.plot(ideal, empirical, color="tab:blue", linewidth=1.5) + ax.plot([0.0, 1.0], [0.0, 1.0], color="0.3", linewidth=1.0, linestyle="--") + ax.plot(ideal, np.clip(ideal + ci, 0.0, 1.0), color="tab:red", linewidth=1.0) + ax.plot(ideal, np.clip(ideal - ci, 0.0, 1.0), color="tab:red", linewidth=1.0) + ax.set_xlim(0.0, 1.0) + ax.set_ylim(0.0, 1.0) + ax.set_xlabel("Ideal Uniform CDF") + ax.set_ylabel("Empirical CDF") + ax.set_title("KS Plot") + return ax + + def plotResidual(self, fit_num: int = 1, handle=None): + diag = self._compute_diagnostics(fit_num) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 3.5))[1] + ax.plot(np.asarray(diag["time"], dtype=float), np.asarray(diag["residual"], dtype=float), color="tab:purple", linewidth=1.0) + ax.axhline(0.0, color="0.4", linewidth=1.0, linestyle="--") + ax.set_xlabel("time [s]") + ax.set_ylabel("count residual") + ax.set_title("Fit Residual") + return ax + + def plotInvGausTrans(self, fit_num: int = 1, handle=None): + diag = self._compute_diagnostics(fit_num) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 3.5))[1] + u = np.asarray(diag["uniforms"], dtype=float) + if u.size: + ax.plot(np.arange(1, u.size + 1), u, color="tab:green", linewidth=1.0) + ax.axhline(0.5, color="0.4", linewidth=1.0, linestyle="--") + ax.set_xlabel("event index") + ax.set_ylabel("time-rescaled transform") + ax.set_title("Inverse-Gaussian/Uniform Transform") + return ax + + def plotSeqCorr(self, fit_num: int = 1, handle=None): + diag = self._compute_diagnostics(fit_num) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 3.5))[1] + lags = np.asarray(diag["acf_lags"], dtype=float) + acf = np.asarray(diag["acf_values"], dtype=float) + if lags.size: + ax.vlines(lags, 0.0, acf, color="tab:orange", linewidth=1.4) + ax.axhline(float(diag["acf_ci"]), color="tab:red", linewidth=1.0) + ax.axhline(-float(diag["acf_ci"]), color="tab:red", linewidth=1.0) + ax.axhline(0.0, color="0.4", linewidth=1.0) + ax.set_xlabel("lag") + ax.set_ylabel("autocorrelation") + ax.set_title("Sequential Correlation of Rescaled ISIs") + return ax + + def plotCoeffs(self, fit_num: int = 1, handle=None): + diag = self._compute_diagnostics(fit_num) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 3.5))[1] + coeffs = np.asarray(diag["coefficients"], dtype=float) + labels = list(np.asarray(diag["coeff_labels"], dtype=object)) + xpos = np.arange(coeffs.size, dtype=float) + ax.axhline(0.0, color="0.6", linewidth=1.0) + ax.plot(xpos, coeffs, "o-", color="tab:blue", linewidth=1.0) + ax.set_xticks(xpos, labels, rotation=45, ha="right") + ax.set_ylabel("coefficient value") + ax.set_title("GLM Coefficients") + return ax @property def lambda_obj(self) -> Covariate: @@ -443,8 +649,24 @@ def getDifflogLL(self, idx: int = 1) -> np.ndarray: base = self.logLL[idx - 1] return self.logLL - base - def plotSummary(self, *_, **__) -> None: - return None + def plotSummary(self, handle=None): + fig = handle if handle is not None else plt.figure(figsize=(10.0, 4.5)) + fig.clear() + axes = fig.subplots(1, 3) + x = np.arange(self.numResults, dtype=float) + labels = list(self.fitNames) + for ax, values, title in zip( + axes, + (self.AIC, self.BIC, self.logLL), + ("AIC", "BIC", "log likelihood"), + strict=False, + ): + ax.bar(x, np.asarray(values, dtype=float), color="tab:blue", alpha=0.8) + ax.set_xticks(x, labels, rotation=30, ha="right") + ax.set_title(title) + ax.grid(axis="y", alpha=0.25) + fig.tight_layout() + return fig class FitResSummary(FitSummary): diff --git a/nstat/history.py b/nstat/history.py index 87fde427..cbf26d01 100644 --- a/nstat/history.py +++ b/nstat/history.py @@ -3,6 +3,10 @@ from collections.abc import Sequence from typing import Any +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt import numpy as np from .core import Covariate, nspikeTrain @@ -98,8 +102,17 @@ def fromStructure(structure: dict[str, Any] | None) -> "History" | None: name=structure.get("name", "History"), ) - def plot(self, *_, **__) -> None: - return None + def plot(self, *_, handle=None, **__): + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 2.2))[1] + ax.clear() + for idx, (start, stop) in enumerate(zip(self.windowTimes[:-1], self.windowTimes[1:]), start=1): + ax.broken_barh([(float(start), float(stop - start))], (idx - 0.4, 0.8), facecolors="tab:blue", alpha=0.6) + ax.set_xlabel("time [s]") + ax.set_ylabel("history bin") + ax.set_yticks(range(1, self.numWindows + 1)) + ax.set_title(self.name) + ax.set_xlim(float(self.windowTimes[0]), float(self.windowTimes[-1])) + return ax HistoryBasis = History diff --git a/nstat/notebook_fidelity_audit.py b/nstat/notebook_fidelity_audit.py index db3c541d..427c9172 100644 --- a/nstat/notebook_fidelity_audit.py +++ b/nstat/notebook_fidelity_audit.py @@ -8,7 +8,11 @@ import nbformat import yaml -from nstat.notebook_parity import extract_figure_contract, load_notebook_parity_notes +from nstat.notebook_parity import ( + audit_notebook_placeholders, + extract_figure_contract, + load_notebook_parity_notes, +) IMG_SRC_RE = re.compile(r']+src="([^"]+)"', re.IGNORECASE) @@ -56,6 +60,7 @@ def build_notebook_fidelity_audit( topic = str(row["topic"]) notebook_path = base / str(row["file"]) figure_contract = extract_figure_contract(notebook_path) + placeholder_audit = audit_notebook_placeholders(notebook_path) python_sections = _count_python_sections(notebook_path) matlab_stem = Path(str(row["source_matlab"])).stem matlab_m_path = help_root / f"{matlab_stem}.m" @@ -72,6 +77,10 @@ def build_notebook_fidelity_audit( "python_expected_figures": int(figure_contract.expected_count) if figure_contract else 0, "python_uses_figure_tracker": figure_contract is not None, "python_has_finalize_call": bool(figure_contract.has_finalize_call) if figure_contract else False, + "python_placeholder_cells": placeholder_audit.placeholder_cells, + "python_tracker_only_cells": placeholder_audit.tracker_only_cells, + "python_contains_placeholders": placeholder_audit.contains_placeholders, + "python_contains_tracker_only_cells": placeholder_audit.contains_tracker_only_cells, } if matlab_available: matlab_sections = _count_matlab_sections(matlab_m_path) diff --git a/nstat/notebook_parity.py b/nstat/notebook_parity.py index 2dee0660..e90f9082 100644 --- a/nstat/notebook_parity.py +++ b/nstat/notebook_parity.py @@ -17,6 +17,7 @@ r"FigureTracker\(\s*topic=['\"](?P[^'\"]+)['\"]\s*,\s*output_root=OUTPUT_ROOT\s*,\s*expected_count=(?P\d+)\s*\)", re.DOTALL, ) +PLACEHOLDER_RE = re.compile(r"(^|\n)\s*pass\b|TODO|FIXME", re.IGNORECASE) @dataclass(frozen=True) @@ -32,6 +33,14 @@ def manifest_path(self, repo_root: Path) -> Path: return self.topic_dir(repo_root) / FIGURE_MANIFEST_NAME +@dataclass(frozen=True) +class NotebookPlaceholderAudit: + placeholder_cells: int + tracker_only_cells: int + contains_placeholders: bool + contains_tracker_only_cells: bool + + def _repo_root() -> Path: return Path(__file__).resolve().parents[1] @@ -69,6 +78,31 @@ def extract_figure_contract(notebook_path: Path) -> NotebookFigureContract | Non ) +def audit_notebook_placeholders(notebook_path: Path) -> NotebookPlaceholderAudit: + notebook = nbformat.read(notebook_path, as_version=4) + placeholder_cells = 0 + tracker_only_cells = 0 + for cell in notebook.cells: + if cell.cell_type != "code": + continue + source = str(cell.get("source", "")) + if PLACEHOLDER_RE.search(source): + placeholder_cells += 1 + non_empty = [line for line in source.splitlines() if line.strip()] + non_comment = [line for line in non_empty if not line.lstrip().startswith("#")] + if non_comment and all( + line.lstrip().startswith("__tracker.") or line.lstrip().startswith("plt.close(") + for line in non_comment + ) and any(line.lstrip().startswith("__tracker.") for line in non_comment): + tracker_only_cells += 1 + return NotebookPlaceholderAudit( + placeholder_cells=placeholder_cells, + tracker_only_cells=tracker_only_cells, + contains_placeholders=placeholder_cells > 0, + contains_tracker_only_cells=tracker_only_cells > 0, + ) + + def reset_notebook_figure_artifacts(repo_root: Path, contract: NotebookFigureContract) -> None: topic_dir = contract.topic_dir(repo_root.resolve()) if not topic_dir.exists(): @@ -134,6 +168,8 @@ def validate_notebook_figure_artifacts( "FIGURE_MANIFEST_NAME", "NOTEBOOK_IMAGE_ROOT", "NotebookFigureContract", + "NotebookPlaceholderAudit", + "audit_notebook_placeholders", "extract_figure_contract", "iter_outstanding_notebook_fidelity", "load_notebook_parity_notes", diff --git a/nstat/parity_report.py b/nstat/parity_report.py index 9885e222..3541d2c4 100644 --- a/nstat/parity_report.py +++ b/nstat/parity_report.py @@ -10,6 +10,11 @@ load_notebook_parity_notes, summarize_notebook_fidelity, ) +from nstat.simulink_fidelity import ( + iter_outstanding_simulink_items, + load_simulink_fidelity_audit, + summarize_simulink_strategies, +) SUMMARY_SECTIONS = ( @@ -79,9 +84,12 @@ def render_parity_report(repo_root: Path | None = None) -> str: payload = load_parity_manifest(repo_root) class_fidelity = load_class_fidelity_audit(repo_root) notebook_fidelity = load_notebook_parity_notes(repo_root) + simulink_fidelity = load_simulink_fidelity_audit(repo_root) class_counts = _summarize_class_fidelity(class_fidelity) notebook_counts = summarize_notebook_fidelity(notebook_fidelity) notebook_partial = iter_outstanding_notebook_fidelity(notebook_fidelity) + simulink_counts = summarize_simulink_strategies(simulink_fidelity) + simulink_outstanding = iter_outstanding_simulink_items(simulink_fidelity) lines = [ "# nSTAT Python Parity Report", "", @@ -128,6 +136,18 @@ def render_parity_report(repo_root: Path | None = None) -> str: for status in ("exact", "high_fidelity", "partial"): lines.append(f"| `{status}` | {notebook_counts.get(status, 0)} |") + lines.extend( + [ + "", + "## Simulink Fidelity Summary", + "", + "| Strategy | Count |", + "|---|---:|", + ] + ) + for status in simulink_fidelity.get("strategy_legend", []): + lines.append(f"| `{status}` | {simulink_counts.get(status, 0)} |") + lines.extend(["", "## Coverage Notes", ""]) lines.append( "- Public API: no missing MATLAB public APIs remain; only the MATLAB help-browser utility is explicitly non-applicable." @@ -138,7 +158,7 @@ def render_parity_report(repo_root: Path | None = None) -> str: f"- Notebook fidelity: workflow coverage is complete, but {len(notebook_partial)} MATLAB-helpfile notebook ports are still marked partial in `tools/notebooks/parity_notes.yml`." ) lines.append( - "- Notebook fidelity audit: structural section/figure comparisons are recorded in `parity/notebook_fidelity.yml`." + "- Notebook fidelity audit: structural section/figure comparisons plus placeholder/tracker-only cell detection are recorded in `parity/notebook_fidelity.yml`." ) else: lines.append("- Notebook fidelity: all tracked MATLAB-helpfile notebook ports are marked high fidelity or exact.") @@ -150,13 +170,19 @@ def render_parity_report(repo_root: Path | None = None) -> str: lines.append( "- Paper examples and docs gallery: all canonical paper examples and committed gallery directories are mapped." ) - priority_remaining = _iter_class_fidelity_rows(class_fidelity, {"partial", "shim_only", "missing"}) + priority_remaining = _iter_class_fidelity_rows(class_fidelity, {"partial", "wrapper_only", "missing"}) if not priority_remaining: - lines.append("- Class fidelity: the class audit reports no partial, shim-only, or missing items.") + lines.append("- Class fidelity: the class audit reports no partial, wrapper-only, or missing items.") else: lines.append( "- Class fidelity: mapping parity is ahead of semantic parity; the audit still reports partial fidelity for several MATLAB-facing classes and workflows." ) + if simulink_outstanding: + lines.append( + f"- Simulink fidelity: {len(simulink_outstanding)} Simulink-backed assets still rely on partial, fallback, or unsupported Python execution paths." + ) + else: + lines.append("- Simulink fidelity: all inventoried Simulink-backed workflows have an explicit Python execution strategy.") lines.extend(["", "## Remaining Mapping Deltas", ""]) outstanding = _iter_outstanding_rows(payload) @@ -187,12 +213,12 @@ def render_parity_report(repo_root: Path | None = None) -> str: lines.extend(["", "## Remaining Class-Fidelity Deltas", ""]) if not priority_remaining: - lines.append("No partial, shim-only, or missing class-fidelity items remain.") + lines.append("No partial, wrapper-only, or missing class-fidelity items remain.") else: for row in priority_remaining: - label = row.get("matlab_name") or row.get("python_symbol") or row.get("matlab_path") - python_target = row.get("python_symbol") or row.get("python_path") - recommendation = row.get("recommended_remediation", []) + label = row.get("matlab_name") or row.get("python_public_name") or row.get("matlab_path") + python_target = row.get("python_public_name") or row.get("python_impl_path") + recommendation = row.get("required_remediation", []) if isinstance(recommendation, list): recommendation_text = recommendation[0] if recommendation else "" else: @@ -201,6 +227,15 @@ def render_parity_report(repo_root: Path | None = None) -> str: detail = recommendation_text or note lines.append(f"- `{label}` -> `{python_target}` [{row['status']}]: {detail}") + lines.extend(["", "## Simulink Fidelity Deltas", ""]) + if not simulink_outstanding: + lines.append("No partial, fallback, or unsupported Simulink execution paths remain in the audit.") + else: + for row in simulink_outstanding: + lines.append( + f"- `{row['model_name']}` -> `{row['model_path']}` [{row['python_strategy']}/{row['current_python_status']}]: {row['chosen_interoperability_strategy']}" + ) + lines.extend(["", "## Justified Non-Applicable Items", ""]) non_applicable = _iter_non_applicable_rows(payload) class_non_applicable = _iter_class_fidelity_rows(class_fidelity, {"not_applicable"}) @@ -214,7 +249,7 @@ def render_parity_report(repo_root: Path | None = None) -> str: lines.append(f"- `{section_name}`: `{label}`. {notes}") for row in class_non_applicable: label = row.get("matlab_name") or row.get("matlab_path") - notes = row.get("known_semantic_differences", []) + notes = row.get("known_remaining_differences", []) if isinstance(notes, list): note_text = notes[0] if notes else "" else: diff --git a/nstat/simulink_fidelity.py b/nstat/simulink_fidelity.py new file mode 100644 index 00000000..63647341 --- /dev/null +++ b/nstat/simulink_fidelity.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def load_simulink_fidelity_audit(repo_root: Path | None = None) -> dict[str, Any]: + base = _repo_root() if repo_root is None else repo_root.resolve() + path = base / "parity" / "simulink_fidelity.yml" + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def summarize_simulink_strategies(payload: dict[str, Any]) -> dict[str, int]: + counts = {status: 0 for status in payload.get("strategy_legend", [])} + for row in payload.get("items", []): + strategy = str(row.get("python_strategy", "")).strip() + if strategy not in counts: + counts[strategy] = 0 + counts[strategy] += 1 + return counts + + +def iter_outstanding_simulink_items(payload: dict[str, Any]) -> list[dict[str, Any]]: + return [ + row + for row in payload.get("items", []) + if row.get("current_python_status") in {"missing", "partial", "unsupported"} + ] + + +__all__ = [ + "iter_outstanding_simulink_items", + "load_simulink_fidelity_audit", + "summarize_simulink_strategies", +] diff --git a/nstat/trial.py b/nstat/trial.py index 5ec62116..f14f40f2 100644 --- a/nstat/trial.py +++ b/nstat/trial.py @@ -3,6 +3,10 @@ from collections.abc import Sequence from typing import Any +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt import numpy as np from .core import Covariate, nspikeTrain @@ -377,6 +381,20 @@ def restoreToOriginal(self) -> None: self.setMaxTime(self.findMaxTime()) self.resetMask() + def plot(self, *_, handle=None, **__): + selected = [idx for idx in range(1, self.numCov + 1)] + fig = handle if handle is not None else plt.figure(figsize=(8.5, max(2.5, 2.2 * max(len(selected), 1)))) + fig.clear() + axes = fig.subplots(len(selected), 1, sharex=True) + if not isinstance(axes, np.ndarray): + axes = np.asarray([axes], dtype=object) + for ax, cov_index in zip(axes.reshape(-1), selected, strict=False): + cov = self.getCov(cov_index) + cov.plot(handle=ax) + ax.set_title(cov.name) + fig.tight_layout() + return fig + def getAllCovLabels(self) -> list[str]: labels: list[str] = [] for index in range(1, self.numCov + 1): @@ -690,8 +708,19 @@ def restoreToOriginal(self, rMask: int = 0) -> None: if rMask == 1: self.resetMask() - def plot(self, *_, **__) -> None: - return None + def plot(self, *_, handle=None, **__): + selected = self.getIndFromMask() + if not selected: + selected = list(range(1, self.numSpikeTrains + 1)) + ax = handle if handle is not None else plt.subplots(1, 1, figsize=(8.0, max(2.5, 0.55 * max(len(selected), 1) + 1.0)))[1] + ax.clear() + for row, neuron_index in enumerate(selected, start=1): + train = self.getNST(neuron_index) + train.plot(dHeight=0.8, yOffset=float(row), currentHandle=ax) + ax.set_ylim(0.25, len(selected) + 0.75) + ax.set_yticks(range(1, len(selected) + 1), [str(item) for item in selected]) + ax.set_title("Spike Train Raster") + return ax def psth( self, @@ -1091,6 +1120,34 @@ def updateTimePartitions(self) -> None: newValMax = min(self.maxTime, validation[1]) self.setTrialPartition([newTrainMin, newTrainMax, newValMin, newValMax]) + def plot(self, *_, handle=None, **__): + cov_count = max(self.covarColl.numCov, 1) + event_count = 1 if self.ev is not None and self.ev.eventTimes.size else 0 + panel_count = 1 + cov_count + event_count + fig = handle if handle is not None else plt.figure(figsize=(9.0, max(4.0, 2.2 * panel_count))) + fig.clear() + axes = fig.subplots(panel_count, 1, sharex=True) + if not isinstance(axes, np.ndarray): + axes = np.asarray([axes], dtype=object) + + cursor = 0 + self.nspikeColl.plot(handle=axes[cursor]) + axes[cursor].set_title("Trial Spike Raster") + cursor += 1 + + for cov_index in range(1, self.covarColl.numCov + 1): + cov = self.covarColl.getCov(cov_index) + cov.plot(handle=axes[cursor]) + axes[cursor].set_title(cov.name) + cursor += 1 + + if event_count: + self.ev.plot(handle=axes[cursor]) + cursor += 1 + + fig.tight_layout() + return fig + def setSampleRate(self, sampleRate: float) -> None: self.sampleRate = float(sampleRate) self.nspikeColl.resample(sampleRate) diff --git a/parity/README.md b/parity/README.md index 0dd1c0ef..8ceccd58 100644 --- a/parity/README.md +++ b/parity/README.md @@ -5,6 +5,7 @@ This directory tracks MATLAB-to-Python parity for the standalone port. Current inventory source: - [`manifest.yml`](./manifest.yml) - [`class_fidelity.yml`](./class_fidelity.yml) +- [`simulink_fidelity.yml`](./simulink_fidelity.yml) - [`report.md`](./report.md) Generated report: @@ -16,6 +17,7 @@ python tools/parity/build_report.py Current headline status: - Public API coverage matches the MATLAB inventory except for the explicitly non-applicable `nstatOpenHelpPage`. - Class-fidelity auditing is tracked separately from name-mapping parity in `class_fidelity.yml`, and it remains intentionally stricter and more conservative than the mapping manifest. +- Simulink-backed workflows are inventoried separately in `simulink_fidelity.yml` so model-dependent execution paths are not conflated with native Python parity. - Help/notebook parity covers the inventoried MATLAB help workflow surface, including the top-level `NeuralSpikeAnalysis_top`, `PaperOverview`, `Examples`, and `ClassDefinitions` navigation pages. - Canonical paper examples, gallery structure, and README/docs presentation are committed and mapped in Python. -- CI now validates API surface, dataset integrity, notebook smoke execution, paper gallery drift, and docs builds. +- CI now validates API surface, dataset integrity, notebook smoke execution, notebook-helpfile full runs, paper gallery drift, and docs builds. diff --git a/parity/class_fidelity.yml b/parity/class_fidelity.yml index f58e70f1..a1eb0c6c 100644 --- a/parity/class_fidelity.yml +++ b/parity/class_fidelity.yml @@ -4,343 +4,521 @@ source_repositories: matlab: https://github.com/cajigaslab/nSTAT python: https://github.com/cajigaslab/nSTAT-python status_legend: - - exact - - high_fidelity - - partial - - shim_only - - missing - - not_applicable +- exact +- high_fidelity +- partial +- wrapper_only +- missing +- not_applicable items: - - matlab_name: SignalObj - kind: class - matlab_path: SignalObj.m - python_symbol: nstat.SignalObj - python_path: nstat/core.py - status: high_fidelity - constructor_parity: Constructor defaults, orientation handling, labels, masks, sample-rate inference, and time-window APIs now mirror MATLAB closely. - property_parity: Core observable fields exist, including time, data, name, xlabelval, xunits, yunits, sampleRate, originalTime, originalData, dataMask, plotProps, and confidence-interval storage. - method_parity: MATLAB-facing methods now cover labels, masking, sub-signals, nearest-time lookup, time-window extraction, merge, arithmetic operators, derivative/derivativeAt, filtering, plotting, restore/reset, mean/std, resampling, and structure export. - default_value_parity: Defaults for labels, units, and sample-rate fallback now match MATLAB closely, including the 1 kHz fallback when sample spacing is ill-conditioned. - shape_and_indexing_parity: Signals use time-by-dimension storage and one-based selector behavior for MATLAB-facing methods. - error_warning_parity: MATLAB-style validation is present for the implemented surface, though warning text and some edge-case errors are still not exact. - output_type_parity: MATLAB-facing methods return SignalObj/Covariate instances where expected. - known_semantic_differences: - - Some specialized MATLAB utilities, plotting options, and correlation helpers remain unported. - - Structure serialization is close but not exhaustive for every MATLAB-only field. - recommended_remediation: - - Add MATLAB-derived fixtures for filter outputs, plotting selectors, and any remaining specialized utility methods. - - matlab_name: Covariate - kind: class - matlab_path: Covariate.m - python_symbol: nstat.Covariate - python_path: nstat/core.py - status: high_fidelity - constructor_parity: Uses the MATLAB-aligned SignalObj constructor shape and supports the Python compatibility aliases for values and units. - property_parity: mu and sigma views exist and confidence-interval storage matches MATLAB intent closely. - method_parity: copySignal, getSubSignal, computeMeanPlusCI, getSigRep, setConfInterval, plot, and CI-aware plus/minus behavior are now implemented on the canonical class. - default_value_parity: Mostly inherited from SignalObj. - shape_and_indexing_parity: Time-by-dimension behavior matches SignalObj and MATLAB-facing one-based selectors are preserved. - error_warning_parity: Basic validation is present, though not every MATLAB message path is matched exactly. - output_type_parity: Covariate methods return Covariate or SignalObj as MATLAB expects for the implemented subset. - known_semantic_differences: - - Some CI plotting options and full structure round-tripping remain lighter than MATLAB. - - More specialized arithmetic/reporting behaviors still need MATLAB-derived fixtures. - recommended_remediation: - - Add MATLAB-derived fixtures for CI plotting and serialized confidence-interval payloads. - - matlab_name: nspikeTrain - kind: class - matlab_path: nspikeTrain.m - python_symbol: nstat.nspikeTrain - python_path: nstat/core.py - status: high_fidelity - constructor_parity: Constructor argument order, defaults, and cached signal-representation setup follow MATLAB closely, including min/max/sample-rate initialization and the makePlots behavior split. - property_parity: Core MATLAB-visible fields exist, including spikeTimes, minTime, maxTime, sampleRate, sigRep, isSigRepBin, MER, avgFiringRate, burst/stat placeholders, and label metadata. - method_parity: MATLAB-facing methods now cover setSigRep, setMinTime, setMaxTime, resample, getSigRep, getSpikeTimes, getISIs, getMinISI, getMaxBinSizeBinary, partitionNST, getFieldVal, computeRate, restoreToOriginal, nstCopy, plot, and structure round-trip. - default_value_parity: Defaults, cache behavior, and restore/resample semantics now track MATLAB much more closely than the earlier simplified implementation. - shape_and_indexing_parity: Spike vectors remain one-dimensional and time-window filtering is inclusive on both ends, matching MATLAB. - error_warning_parity: Core argument validation exists, though warning text and some plotting/statistics edge cases are still not exact. - output_type_parity: Signal representation returns SignalObj and rate conversion returns SignalObj as expected. - known_semantic_differences: - - Several ISI-plot helper methods remain unported or lighter than MATLAB. - - Burst metrics remain approximated rather than fully MATLAB-equivalent. - recommended_remediation: - - Port the remaining ISI plotting helpers and burst-detection detail from MATLAB. - - Add MATLAB-derived fixtures for partitionNST and burst/statistics outputs. - - matlab_name: nstColl - kind: class - matlab_path: nstColl.m - python_symbol: nstat.nstColl - python_path: nstat/trial.py - status: high_fidelity - constructor_parity: Empty construction, direct sequence construction, and MATLAB-style collection state initialization now match MATLAB much more closely. - property_parity: Core MATLAB-visible fields exist, including nstrain, numSpikeTrains, minTime, maxTime, sampleRate, neuronMask, and neighbors. - method_parity: MATLAB-facing collection methods are now first-class, including addToColl, addSingleSpikeToColl, merge, getNST, name/index lookup, masking, neighborhood management, dataToMatrix, ensemble-covariate helpers, restoreToOriginal, psth, and psthGLM. - default_value_parity: Defaults for masks, sample rate, and min/max time now track MATLAB collection semantics closely. - shape_and_indexing_parity: MATLAB-facing one-based getNST is preserved. - error_warning_parity: Core validation is present, though MATLAB warning text and some edge-case messages still differ. - output_type_parity: PSTH returns Covariate. - known_semantic_differences: - - Some plotting/statistics helpers and lower-level utility methods from MATLAB are still absent. - recommended_remediation: - - Add MATLAB-derived fixtures for neighbor masks, ensemble covariates, and PSTH outputs. - - Port any remaining collection utilities that surface in MATLAB helpfiles. - - matlab_name: Trial - kind: class - matlab_path: Trial.m - python_symbol: nstat.Trial - python_path: nstat/trial.py - status: high_fidelity - constructor_parity: The canonical Python Trial now accepts MATLAB-style spike, covariate, event, history, and ensemble-history inputs and normalizes trial state similarly to MATLAB. - property_parity: Core MATLAB-facing state is now present, including nspikeColl, covarColl, ev, history, ensCovHist, ensCovColl, sampleRate, minTime, maxTime, covMask, ensCovMask, neuronMask, trainingWindow, and validationWindow. - method_parity: The MATLAB trial workflow is much richer now, covering event/history setup, partitioning, sample-rate and time consistency, neuron/covariate masking, design-matrix generation, history/ensemble covariates, label extraction, and restore/reset helpers. - default_value_parity: Default object state and partition behavior are much closer to MATLAB than the earlier thin implementation. - shape_and_indexing_parity: Core one-based neuron selection is preserved via getSpikeVector. - error_warning_parity: Core validation is present, but some MATLAB warning and edge-case pathways still differ. - output_type_parity: Matrix-producing methods intentionally return NumPy arrays, while MATLAB-facing object-producing workflows return Trial/CovColl/nstColl-compatible objects where expected. - known_semantic_differences: - - Some MATLAB plotting, partition-serialization, and specialized workflow helpers remain unported. - recommended_remediation: - - Add dataset-backed fixtures for trial partitioning, ensemble-history construction, and design-matrix parity. - - Port the remaining specialized Trial helpers used only in MATLAB helpfiles. - - matlab_name: TrialConfig - kind: class - matlab_path: TrialConfig.m - python_symbol: nstat.TrialConfig - python_path: nstat/trial.py - status: high_fidelity - constructor_parity: The constructor now matches MATLAB intent much more closely, including covMask, sampleRate, history, ensCovHist, ensCovMask, covLag, and name handling. - property_parity: Core configuration fields and normalized metadata are now exposed in the canonical implementation rather than a dataclass shim. - method_parity: MATLAB-facing methods now include naming, structure round-trip, and setConfig application against Trial state. - default_value_parity: Defaults for empty masks/configs and name handling are close to MATLAB. - shape_and_indexing_parity: N/A for this class. - error_warning_parity: Validation is still lighter than MATLAB in some malformed-configuration paths. - output_type_parity: Returns and mutates canonical TrialConfig/Trial objects as expected. - known_semantic_differences: - - Some MATLAB normalization and validation branches remain looser in Python. - recommended_remediation: - - Add malformed-config fixtures from MATLAB to tighten validation and default coercion behavior. - - matlab_name: ConfigColl - kind: class - matlab_path: ConfigColl.m - python_symbol: nstat.ConfigColl - python_path: nstat/trial.py - status: high_fidelity - constructor_parity: Supports MATLAB-style collections of TrialConfig objects, string-named configs, and empty config placeholders. - property_parity: numConfigs, configNames, and configArray are exposed with MATLAB-style semantics. - method_parity: addConfig, getConfig, setConfig, getConfigNames, setConfigNames, getSubsetConfigs, and structure round-trip are now implemented in the canonical collection. - default_value_parity: Empty-config and naming defaults now align closely with MATLAB behavior. - shape_and_indexing_parity: One-based getConfig behavior is preserved. - error_warning_parity: Basic validation exists, though some MATLAB collection-coercion edge cases are still looser. - output_type_parity: Returns TrialConfig instances. - known_semantic_differences: - - Some MATLAB-specific collection manipulation helpers remain unported. - recommended_remediation: - - Add fixture-backed tests for edge-case config coercion and selection semantics. - - matlab_name: Analysis - kind: class - matlab_path: Analysis.m - python_symbol: nstat.Analysis - python_path: nstat/analysis.py - status: high_fidelity - constructor_parity: Analysis remains a static-workflow class in Python, but the MATLAB-facing entry points are now aligned around RunAnalysisForNeuron and RunAnalysisForAllNeurons semantics. - property_parity: N/A for the static workflow surface. - method_parity: Canonical analysis now restores trial state, applies ConfigColl entries, builds MATLAB-style design matrices and labels, and returns richer FitResult metadata for per-neuron and all-neuron workflows. - default_value_parity: Default fitting behavior and Poisson-GLM selection are much closer to the MATLAB workflow defaults. - shape_and_indexing_parity: MATLAB-facing one-based neuron numbering remains available through the public entry points. - error_warning_parity: Core validation is present, though algorithm-selection and advanced option warnings remain thinner than MATLAB. - output_type_parity: Returns MATLAB-facing FitResult/FitResSummary-compatible objects with richer metadata than the previous simplified implementation. - known_semantic_differences: - - Advanced MATLAB algorithm-selection, cross-validation, and plotting/reporting branches are still incomplete. - recommended_remediation: - - Add dataset-backed numerical parity fixtures for canonical analysis workflows. - - Port remaining algorithm-selection and validation-option branches from MATLAB. - - matlab_name: FitResult - kind: class - matlab_path: FitResult.m - python_symbol: nstat.FitResult - python_path: nstat/fit.py - status: high_fidelity - constructor_parity: The canonical constructor now supports both the legacy simplified Python path and a MATLAB-style metadata-rich construction path. - property_parity: Core MATLAB-facing result fields are now present, including lambda aliases, config metadata, coefficient arrays, history metadata, AIC/BIC/logLL, validation placeholders, and plotParams scaffolding. - method_parity: getCoeffs, getHistCoeffs, mergeResults, and structure round-trip now operate on the richer MATLAB-style result surface. - default_value_parity: Default result metadata and placeholder fields are much closer to MATLAB than the earlier lightweight container. - shape_and_indexing_parity: N/A for this class. - error_warning_parity: Validation is still lighter than MATLAB in malformed-structure and reporting edge cases. - output_type_parity: Returns canonical FitResult objects with MATLAB-style aliases and list/array fields. - known_semantic_differences: - - Plotting, KS/inverse-Gaussian reporting detail, and some summary utilities remain stubbed. - recommended_remediation: - - Add MATLAB-derived golden fixtures for coefficient metadata and validation/report payloads. - - Port the remaining plotting/report helpers used by the MATLAB toolbox. - - matlab_name: FitResSummary - kind: class - matlab_path: FitResSummary.m - python_symbol: nstat.FitResSummary - python_path: nstat/fit.py - status: high_fidelity - constructor_parity: Summary objects now aggregate MATLAB-style FitResult collections directly. - property_parity: Core summary fields exist, including fitResCell, numNeurons, numResults, fitNames, neuronNumbers, AIC, BIC, logLL, and KSStats. - method_parity: MATLAB-style difference helpers are implemented through getDiffAIC, getDiffBIC, and getDifflogLL. - default_value_parity: Summary initialization is close for the implemented metadata surface. - shape_and_indexing_parity: N/A for this class. - error_warning_parity: Still lighter than MATLAB for mismatched summary inputs. - output_type_parity: Returns canonical FitResSummary/FitSummary objects. - known_semantic_differences: - - Summary plotting and richer report/table exports are still not MATLAB-equivalent. - recommended_remediation: - - Add golden fixtures for multi-neuron summary aggregation and remaining report outputs. - - matlab_name: CIF - kind: class - matlab_path: CIF.m - python_symbol: nstat.CIF - python_path: nstat/cif.py - status: high_fidelity - constructor_parity: The canonical CIF object now accepts MATLAB-style beta, name, fitType, history, and spike-train metadata. - property_parity: Core modeling metadata is present for fitting and simulation workflows. - method_parity: evaluate, to_covariate, simulateCIFByThinningFromLambda, and from_linear_terms provide the MATLAB-facing simulation and conversion surface used by current workflows. - default_value_parity: Default fitType and basic constructor normalization are close to MATLAB for the implemented workflow subset. - shape_and_indexing_parity: Vector/matrix handling is aligned to MATLAB-style time-by-feature design matrices. - error_warning_parity: Validation is present, though advanced MATLAB error paths remain thinner. - output_type_parity: Returns rate arrays, Covariates, and spike-train collections in the expected workflow positions. - known_semantic_differences: - - Some history-aware, decoding-specific, and reporting helpers remain unported. - recommended_remediation: - - Add MATLAB-derived fixtures for CIF evaluation and thinning outputs. - - Port the remaining decoding-oriented CIF helpers. - - matlab_name: DecodingAlgorithms - kind: class - matlab_path: DecodingAlgorithms.m - python_symbol: nstat.DecodingAlgorithms - python_path: nstat/decoding_algorithms.py - status: high_fidelity - constructor_parity: Static-method MATLAB class semantics are preserved; the PascalCase module now re-exports the canonical implementation directly rather than using a shim-first wrapper. - property_parity: N/A for the static decoding API surface. - method_parity: MATLAB-facing decoding entry points now include PPDecode_predict, PPDecode_updateLinear, PPDecodeFilterLinear, PPDecodeFilter, PPHybridFilterLinear, and PPHybridFilter alongside the existing generic helpers. - default_value_parity: Core defaults for fitType, delta/binwidth, empty history terms, and initial-state handling now match MATLAB intent closely for the implemented workflows. - shape_and_indexing_parity: MATLAB-style state and covariance output shapes are preserved, including x_p/x_u and W_p/W_u tensor layouts plus hybrid-model probability/state-bank outputs. - error_warning_parity: Validation is much closer to MATLAB for signature and shape handling, though some advanced unsupported CIF workflows still raise Python-specific exceptions. - output_type_parity: MATLAB-facing methods now return tuple outputs and state/covariance tensors instead of only Python-specific dictionaries. - known_semantic_differences: - - Target-estimation augmentation and some advanced CIF-driven symbolic workflows remain thinner than MATLAB. - recommended_remediation: - - Add MATLAB-derived numerical fixtures for DecodingExample, DecodingExampleWithHist, StimulusDecode2D, and HybridFilterExample. - - Port the remaining target-estimation and symbolic-CIF branches from the MATLAB toolbox. - - matlab_name: History - kind: class - matlab_path: History.m - python_symbol: nstat.History - python_path: nstat/history.py - status: high_fidelity - constructor_parity: History now uses MATLAB-style windowTimes construction with optional min/max metadata. - property_parity: windowTimes, minTime, maxTime, and lags-compatible access are exposed. - method_parity: setWindow, computeHistory/compute_history, structure round-trip, and CovColl-producing history workflows are now implemented for single trains, train collections, and trial history use. - default_value_parity: Window-boundary defaults are close to MATLAB for the implemented history workflows. - shape_and_indexing_parity: WindowTimes are interpreted as MATLAB-style consecutive lag boundaries. - error_warning_parity: Core validation is present, though MATLAB warning text and some malformed-input branches remain thinner. - output_type_parity: Returns CovariateCollection outputs in the MATLAB-facing workflows that consume History objects. - known_semantic_differences: - - Plotting and some specialized history-basis utilities remain unported. - recommended_remediation: - - Add MATLAB-derived fixtures for history-window outputs and multi-neuron history collections. - - matlab_name: Events - kind: class - matlab_path: Events.m - python_symbol: nstat.Events - python_path: nstat/events.py - status: high_fidelity - constructor_parity: Constructor now tracks MATLAB eventTimes, eventLabels, and eventColor semantics, including label-count validation. - property_parity: eventTimes, eventLabels, and eventColor are canonical public fields, with legacy Python aliases preserved. - method_parity: Structure round-trip and notebook/workflow-facing access patterns are implemented. - default_value_parity: Empty-label and default-color behavior are close to MATLAB for the implemented workflow subset. - shape_and_indexing_parity: Event vectors are stored in MATLAB-style flat time/label arrays. - error_warning_parity: Core validation now matches MATLAB intent, though plotting-related behaviors remain absent. - output_type_parity: Returns canonical Events objects. - known_semantic_differences: - - Plotting and some MATLAB-specific display behaviors are still unported. - recommended_remediation: - - Add notebook-backed fixtures for event serialization and display workflows. - - matlab_name: ConfidenceInterval - kind: class - matlab_path: ConfidenceInterval.m - python_symbol: nstat.ConfidenceInterval - python_path: nstat/confidence_interval.py - status: high_fidelity - constructor_parity: Basic time-and-bounds construction aligns with MATLAB intent. - property_parity: lower and upper accessors plus color metadata are exposed. - method_parity: Color assignment, plotting, and arithmetic composition with scalar signals and other confidence intervals are implemented for the MATLAB-facing workflows used by Covariate. - default_value_parity: Default color and time/bounds normalization are close to MATLAB. - shape_and_indexing_parity: Bounds are stored in MATLAB-style n x 2 lower/upper form. - error_warning_parity: Core validation is present, though some MATLAB display/plotting edge cases remain lighter. - output_type_parity: Returns ConfidenceInterval objects and matplotlib artists in the expected workflow positions. - known_semantic_differences: - - Full MATLAB serialization/display semantics remain lighter than the original toolbox. - recommended_remediation: - - Add MATLAB-derived fixtures for serialized confidence-interval payloads and plot styling. - - matlab_name: CovColl - kind: class - matlab_path: CovColl.m - python_symbol: nstat.CovColl - python_path: nstat/trial.py - status: high_fidelity - constructor_parity: CovColl now supports MATLAB-style direct construction, empty initialization, and nested collection ingestion. - property_parity: Core collection state exists, including covArray, covDimensions, numCov, minTime, maxTime, covMask, covShift, sampleRate, and original timing metadata. - method_parity: MATLAB-facing collection methods are now first-class, covering add/remove, name/index lookup, mask selectors, time-window restriction, resampling, matrixWithTime, dataToMatrix, shift/reset, label extraction, and restoreToOriginal. - default_value_parity: Default mask, shift, sample-rate, and timing behavior now track MATLAB collection semantics closely. - shape_and_indexing_parity: Shared-time enforcement is implemented. - error_warning_parity: Core validation is present, though some MATLAB warning text and malformed-selector branches are still thinner. - output_type_parity: Returns Covariate and CovariateCollection-compatible outputs across MATLAB-facing workflows. - known_semantic_differences: - - Some structure serialization and rarely used helper methods remain unported. - recommended_remediation: - - Add MATLAB-derived fixtures for selector masks, time-window coercion, and serialized collection state. - - matlab_name: getPaperDataDirs - kind: function - matlab_path: getPaperDataDirs.m - python_symbol: nstat.getPaperDataDirs - python_path: nstat/data_manager.py - status: high_fidelity - constructor_parity: N/A - property_parity: N/A - method_parity: Python helper exposes MATLAB-style name and standalone repo semantics. - default_value_parity: Defaults to the Python repo's independent example-data cache instead of a MATLAB checkout path. - shape_and_indexing_parity: N/A - error_warning_parity: Close for the Python use case. - output_type_parity: Returns directory paths as a Python tuple/list structure rather than MATLAB cell arrays. - known_semantic_differences: - - Python returns native path types/strings rather than MATLAB cells. - recommended_remediation: - - Add a MATLAB-reference fixture for the directory tuple shape if stricter parity is needed. - - matlab_name: nSTAT_Install - kind: function - matlab_path: nSTAT_Install.m - python_symbol: nstat.nSTAT_Install - python_path: nstat/install.py - status: high_fidelity - constructor_parity: N/A - property_parity: N/A - method_parity: Python installer covers data download, docs rebuild, and MATLAB-compatible flags while explicitly documenting the Python-only no-op path-preference behavior. - default_value_parity: Defaults are aligned to standalone Python packaging while preserving MATLAB-facing flag names where reasonable. - shape_and_indexing_parity: N/A - error_warning_parity: Installer status output and failure reporting are validated in Python, with MATLAB path warnings intentionally replaced by structured Python notes. - output_type_parity: Returns Python dictionaries/status text rather than MATLAB console-only behavior. - known_semantic_differences: - - MATLAB path management is intentionally non-applicable in Python. - recommended_remediation: - - Keep documenting the no-op compatibility behavior and test installer status outputs. - - matlab_name: nstatOpenHelpPage - kind: function - matlab_path: nstatOpenHelpPage.m - python_symbol: null - python_path: null - status: not_applicable - constructor_parity: N/A - property_parity: N/A - method_parity: MATLAB help-browser integration has no direct standalone Python equivalent. - default_value_parity: N/A - shape_and_indexing_parity: N/A - error_warning_parity: N/A - output_type_parity: N/A - known_semantic_differences: - - Python uses Sphinx docs pages instead of the MATLAB help browser. - recommended_remediation: - - None. +- matlab_name: SignalObj + kind: class + matlab_path: SignalObj.m + python_public_name: nstat.SignalObj + python_impl_path: nstat/core.py + status: high_fidelity + constructor_parity: Constructor defaults, orientation handling, labels, masks, sample-rate + inference, and time-window APIs now mirror MATLAB closely. + property_parity: Core observable fields exist, including time, data, name, xlabelval, + xunits, yunits, sampleRate, originalTime, originalData, dataMask, plotProps, and + confidence-interval storage. + method_parity: MATLAB-facing methods now cover labels, masking, sub-signals, nearest-time + lookup, time-window extraction, merge, arithmetic operators, derivative/derivativeAt, + filtering, plotting, restore/reset, mean/std, resampling, and structure export. + defaults_parity: Defaults for labels, units, and sample-rate fallback now match + MATLAB closely, including the 1 kHz fallback when sample spacing is ill-conditioned. + indexing_parity: Signals use time-by-dimension storage and one-based selector behavior + for MATLAB-facing methods. + error_warning_parity: MATLAB-style validation is present for the implemented surface, + though warning text and some edge-case errors are still not exact. + output_type_parity: MATLAB-facing methods return SignalObj/Covariate instances where + expected. + known_remaining_differences: + - Some specialized MATLAB utilities, plotting options, and correlation helpers remain + unported. + - Structure serialization is close but not exhaustive for every MATLAB-only field. + required_remediation: + - Add MATLAB-derived fixtures for filter outputs, plotting selectors, and any remaining + specialized utility methods. + plotting_report_parity: Core plotting is implemented; some MATLAB-only plot selectors, + spectral utilities, and report-style helpers remain lighter. +- matlab_name: Covariate + kind: class + matlab_path: Covariate.m + python_public_name: nstat.Covariate + python_impl_path: nstat/core.py + status: high_fidelity + constructor_parity: Uses the MATLAB-aligned SignalObj constructor shape and supports + the Python compatibility aliases for values and units. + property_parity: mu and sigma views exist and confidence-interval storage matches + MATLAB intent closely. + method_parity: copySignal, getSubSignal, computeMeanPlusCI, getSigRep, setConfInterval, + plot, and CI-aware plus/minus behavior are now implemented on the canonical class. + defaults_parity: Mostly inherited from SignalObj. + indexing_parity: Time-by-dimension behavior matches SignalObj and MATLAB-facing + one-based selectors are preserved. + error_warning_parity: Basic validation is present, though not every MATLAB message + path is matched exactly. + output_type_parity: Covariate methods return Covariate or SignalObj as MATLAB expects + for the implemented subset. + known_remaining_differences: + - Some CI plotting options and full structure round-tripping remain lighter than + MATLAB. + - More specialized arithmetic/reporting behaviors still need MATLAB-derived fixtures. + required_remediation: + - Add MATLAB-derived fixtures for CI plotting and serialized confidence-interval + payloads. + plotting_report_parity: Core signal and confidence-interval plotting works; some + MATLAB CI styling/report variations remain lighter. +- matlab_name: nspikeTrain + kind: class + matlab_path: nspikeTrain.m + python_public_name: nstat.nspikeTrain + python_impl_path: nstat/core.py + status: high_fidelity + constructor_parity: Constructor argument order, defaults, and cached signal-representation + setup follow MATLAB closely, including min/max/sample-rate initialization and + the makePlots behavior split. + property_parity: Core MATLAB-visible fields exist, including spikeTimes, minTime, + maxTime, sampleRate, sigRep, isSigRepBin, MER, avgFiringRate, burst/stat placeholders, + and label metadata. + method_parity: MATLAB-facing methods now cover setSigRep, setMinTime, setMaxTime, + resample, getSigRep, getSpikeTimes, getISIs, getMinISI, getMaxBinSizeBinary, partitionNST, + getFieldVal, computeRate, restoreToOriginal, nstCopy, plot, and structure round-trip. + defaults_parity: Defaults, cache behavior, and restore/resample semantics now track + MATLAB much more closely than the earlier simplified implementation. + indexing_parity: Spike vectors remain one-dimensional and time-window filtering + is inclusive on both ends, matching MATLAB. + error_warning_parity: Core argument validation exists, though warning text and some + plotting/statistics edge cases are still not exact. + output_type_parity: Signal representation returns SignalObj and rate conversion + returns SignalObj as expected. + known_remaining_differences: + - Several ISI-plot helper methods remain unported or lighter than MATLAB. + - Burst metrics remain approximated rather than fully MATLAB-equivalent. + required_remediation: + - Port the remaining ISI plotting helpers and burst-detection detail from MATLAB. + - Add MATLAB-derived fixtures for partitionNST and burst/statistics outputs. + plotting_report_parity: Raster/basic plotting works; ISI, burst, and reporting helpers + remain thinner than MATLAB. +- matlab_name: nstColl + kind: class + matlab_path: nstColl.m + python_public_name: nstat.nstColl + python_impl_path: nstat/trial.py + status: high_fidelity + constructor_parity: Empty construction, direct sequence construction, and MATLAB-style + collection state initialization now match MATLAB much more closely. + property_parity: Core MATLAB-visible fields exist, including nstrain, numSpikeTrains, + minTime, maxTime, sampleRate, neuronMask, and neighbors. + method_parity: MATLAB-facing collection methods are now first-class, including addToColl, + addSingleSpikeToColl, merge, getNST, name/index lookup, masking, neighborhood + management, dataToMatrix, ensemble-covariate helpers, restoreToOriginal, psth, + and psthGLM. + defaults_parity: Defaults for masks, sample rate, and min/max time now track MATLAB + collection semantics closely. + indexing_parity: MATLAB-facing one-based getNST is preserved. + error_warning_parity: Core validation is present, though MATLAB warning text and + some edge-case messages still differ. + output_type_parity: PSTH returns Covariate. + known_remaining_differences: + - Some plotting/statistics helpers and lower-level utility methods from MATLAB are + still absent. + required_remediation: + - Add MATLAB-derived fixtures for neighbor masks, ensemble covariates, and PSTH + outputs. + - Port any remaining collection utilities that surface in MATLAB helpfiles. + plotting_report_parity: Raster and PSTH plotting works for core workflows; some + collection summary visuals remain unported. +- matlab_name: Trial + kind: class + matlab_path: Trial.m + python_public_name: nstat.Trial + python_impl_path: nstat/trial.py + status: high_fidelity + constructor_parity: The canonical Python Trial now accepts MATLAB-style spike, covariate, + event, history, and ensemble-history inputs and normalizes trial state similarly + to MATLAB. + property_parity: Core MATLAB-facing state is now present, including nspikeColl, + covarColl, ev, history, ensCovHist, ensCovColl, sampleRate, minTime, maxTime, + covMask, ensCovMask, neuronMask, trainingWindow, and validationWindow. + method_parity: The MATLAB trial workflow is much richer now, covering event/history + setup, partitioning, sample-rate and time consistency, neuron/covariate masking, + design-matrix generation, history/ensemble covariates, label extraction, and restore/reset + helpers. + defaults_parity: Default object state and partition behavior are much closer to + MATLAB than the earlier thin implementation. + indexing_parity: Core one-based neuron selection is preserved via getSpikeVector. + error_warning_parity: Core validation is present, but some MATLAB warning and edge-case + pathways still differ. + output_type_parity: Matrix-producing methods intentionally return NumPy arrays, + while MATLAB-facing object-producing workflows return Trial/CovColl/nstColl-compatible + objects where expected. + known_remaining_differences: + - Some MATLAB plotting, partition-serialization, and specialized workflow helpers + remain unported. + required_remediation: + - Add dataset-backed fixtures for trial partitioning, ensemble-history construction, + and design-matrix parity. + - Port the remaining specialized Trial helpers used only in MATLAB helpfiles. + plotting_report_parity: Notebook-facing trial plots work, but several MATLAB display, + partition-summary, and serialization views remain lighter. +- matlab_name: TrialConfig + kind: class + matlab_path: TrialConfig.m + python_public_name: nstat.TrialConfig + python_impl_path: nstat/trial.py + status: high_fidelity + constructor_parity: The constructor now matches MATLAB intent much more closely, + including covMask, sampleRate, history, ensCovHist, ensCovMask, covLag, and name + handling. + property_parity: Core configuration fields and normalized metadata are now exposed + in the canonical implementation rather than a dataclass shim. + method_parity: MATLAB-facing methods now include naming, structure round-trip, and + setConfig application against Trial state. + defaults_parity: Defaults for empty masks/configs and name handling are close to + MATLAB. + indexing_parity: N/A for this class. + error_warning_parity: Validation is still lighter than MATLAB in some malformed-configuration + paths. + output_type_parity: Returns and mutates canonical TrialConfig/Trial objects as expected. + known_remaining_differences: + - Some MATLAB normalization and validation branches remain looser in Python. + required_remediation: + - Add malformed-config fixtures from MATLAB to tighten validation and default coercion + behavior. + plotting_report_parity: N/A +- matlab_name: ConfigColl + kind: class + matlab_path: ConfigColl.m + python_public_name: nstat.ConfigColl + python_impl_path: nstat/trial.py + status: high_fidelity + constructor_parity: Supports MATLAB-style collections of TrialConfig objects, string-named + configs, and empty config placeholders. + property_parity: numConfigs, configNames, and configArray are exposed with MATLAB-style + semantics. + method_parity: addConfig, getConfig, setConfig, getConfigNames, setConfigNames, + getSubsetConfigs, and structure round-trip are now implemented in the canonical + collection. + defaults_parity: Empty-config and naming defaults now align closely with MATLAB + behavior. + indexing_parity: One-based getConfig behavior is preserved. + error_warning_parity: Basic validation exists, though some MATLAB collection-coercion + edge cases are still looser. + output_type_parity: Returns TrialConfig instances. + known_remaining_differences: + - Some MATLAB-specific collection manipulation helpers remain unported. + required_remediation: + - Add fixture-backed tests for edge-case config coercion and selection semantics. + plotting_report_parity: N/A +- matlab_name: Analysis + kind: class + matlab_path: Analysis.m + python_public_name: nstat.Analysis + python_impl_path: nstat/analysis.py + status: partial + constructor_parity: Analysis remains a static-workflow class in Python, but the + MATLAB-facing entry points are now aligned around RunAnalysisForNeuron and RunAnalysisForAllNeurons + semantics. + property_parity: N/A for the static workflow surface. + method_parity: Canonical analysis now restores trial state, applies ConfigColl entries, + builds MATLAB-style design matrices and labels, returns richer FitResult metadata + for per-neuron and all-neuron workflows, and feeds the MATLAB-facing diagnostic + plotting surface exposed through FitResult. + defaults_parity: Default fitting behavior and Poisson-GLM selection are much closer + to the MATLAB workflow defaults. + indexing_parity: MATLAB-facing one-based neuron numbering remains available through + the public entry points. + error_warning_parity: Core validation is present, though algorithm-selection and + advanced option warnings remain thinner than MATLAB. + output_type_parity: Returns MATLAB-facing FitResult/FitResSummary-compatible objects + with richer metadata than the previous simplified implementation. + known_remaining_differences: + - Advanced MATLAB algorithm-selection, cross-validation, and plotting/reporting + branches are still incomplete. + required_remediation: + - Add dataset-backed numerical parity fixtures for canonical analysis workflows. + - Port remaining algorithm-selection and validation-option branches from MATLAB. + plotting_report_parity: KS, inverse-Gaussian, coefficient, residual, and summary + plots now execute on canonical Analysis output, but advanced algorithm-selection, + report layout, and validation branches are still thinner than MATLAB. +- matlab_name: FitResult + kind: class + matlab_path: FitResult.m + python_public_name: nstat.FitResult + python_impl_path: nstat/fit.py + status: partial + constructor_parity: The canonical constructor now supports both the legacy simplified + Python path and a MATLAB-style metadata-rich construction path. + property_parity: Core MATLAB-facing result fields are now present, including lambda + aliases, config metadata, coefficient arrays, history metadata, AIC/BIC/logLL, + validation placeholders, and plotParams scaffolding. + method_parity: getCoeffs, getHistCoeffs, mergeResults, structure round-trip, KS/inverse-Gaussian + diagnostics, residual computation, coefficient plotting, and report plotting now + operate on the richer MATLAB-style result surface. + defaults_parity: Default result metadata and placeholder fields are much closer + to MATLAB than the earlier lightweight container. + indexing_parity: N/A for this class. + error_warning_parity: Validation is still lighter than MATLAB in malformed-structure + and reporting edge cases. + output_type_parity: Returns canonical FitResult objects with MATLAB-style aliases + and list/array fields. + known_remaining_differences: + - Plotting/report methods now execute, but their numerical detail and layout are + still lighter than MATLAB. + required_remediation: + - Add MATLAB-derived golden fixtures for coefficient metadata and validation/report + payloads. + - Port the remaining plotting/report helpers used by the MATLAB toolbox. + plotting_report_parity: Result plotting/report methods now exist on the canonical + object, but they still need fuller MATLAB-style diagnostic detail and fixture-backed + validation. +- matlab_name: FitResSummary + kind: class + matlab_path: FitResSummary.m + python_public_name: nstat.FitResSummary + python_impl_path: nstat/fit.py + status: partial + constructor_parity: Summary objects now aggregate MATLAB-style FitResult collections + directly. + property_parity: Core summary fields exist, including fitResCell, numNeurons, numResults, + fitNames, neuronNumbers, AIC, BIC, logLL, and KSStats. + method_parity: MATLAB-style difference helpers are implemented through getDiffAIC, + getDiffBIC, getDifflogLL, and a basic plotSummary report surface. + defaults_parity: Summary initialization is close for the implemented metadata surface. + indexing_parity: N/A for this class. + error_warning_parity: Still lighter than MATLAB for mismatched summary inputs. + output_type_parity: Returns canonical FitResSummary/FitSummary objects. + known_remaining_differences: + - Summary plotting now exists, but richer MATLAB report/table exports are still + not MATLAB-equivalent. + required_remediation: + - Add golden fixtures for multi-neuron summary aggregation and remaining report + outputs. + plotting_report_parity: Summary plotting and richer report/table exports remain + partial relative to MATLAB. +- matlab_name: CIF + kind: class + matlab_path: CIF.m + python_public_name: nstat.CIF + python_impl_path: nstat/cif.py + status: partial + constructor_parity: The canonical CIF object now accepts MATLAB-style beta, name, + fitType, history, and spike-train metadata. + property_parity: Core modeling metadata is present for fitting and simulation workflows. + method_parity: evaluate, to_covariate, simulateCIFByThinningFromLambda, and from_linear_terms + provide the MATLAB-facing simulation and conversion surface used by current workflows. + defaults_parity: Default fitType and basic constructor normalization are close to + MATLAB for the implemented workflow subset. + indexing_parity: Vector/matrix handling is aligned to MATLAB-style time-by-feature + design matrices. + error_warning_parity: Validation is present, though advanced MATLAB error paths + remain thinner. + output_type_parity: Returns rate arrays, Covariates, and spike-train collections + in the expected workflow positions. + known_remaining_differences: + - Some history-aware, decoding-specific, and reporting helpers remain unported. + required_remediation: + - Add MATLAB-derived fixtures for CIF evaluation and thinning outputs. + - Port the remaining decoding-oriented CIF helpers. + plotting_report_parity: Simulation/report plotting is limited; downstream notebooks + generate figures with helper code rather than a full MATLAB-equivalent CIF report + API. +- matlab_name: DecodingAlgorithms + kind: class + matlab_path: DecodingAlgorithms.m + python_public_name: nstat.DecodingAlgorithms + python_impl_path: nstat/decoding_algorithms.py + status: partial + constructor_parity: Static-method MATLAB class semantics are preserved; the PascalCase + module now re-exports the canonical implementation directly rather than using + a shim-first wrapper. + property_parity: N/A for the static decoding API surface. + method_parity: MATLAB-facing decoding entry points now include PPDecode_predict, + PPDecode_updateLinear, PPDecodeFilterLinear, PPDecodeFilter, PPHybridFilterLinear, + and PPHybridFilter alongside the existing generic helpers. + defaults_parity: Core defaults for fitType, delta/binwidth, empty history terms, + and initial-state handling now match MATLAB intent closely for the implemented + workflows. + indexing_parity: MATLAB-style state and covariance output shapes are preserved, + including x_p/x_u and W_p/W_u tensor layouts plus hybrid-model probability/state-bank + outputs. + error_warning_parity: Validation is much closer to MATLAB for signature and shape + handling, though some advanced unsupported CIF workflows still raise Python-specific + exceptions. + output_type_parity: MATLAB-facing methods now return tuple outputs and state/covariance + tensors instead of only Python-specific dictionaries. + known_remaining_differences: + - Target-estimation augmentation and some advanced CIF-driven symbolic workflows + remain thinner than MATLAB. + required_remediation: + - Add MATLAB-derived numerical fixtures for DecodingExample, DecodingExampleWithHist, + StimulusDecode2D, and HybridFilterExample. + - Port the remaining target-estimation and symbolic-CIF branches from the MATLAB + toolbox. + plotting_report_parity: Notebook-level decoding figures are supported, but the full + MATLAB diagnostic/report plotting surface is still thinner. +- matlab_name: History + kind: class + matlab_path: History.m + python_public_name: nstat.History + python_impl_path: nstat/history.py + status: high_fidelity + constructor_parity: History now uses MATLAB-style windowTimes construction with + optional min/max metadata. + property_parity: windowTimes, minTime, maxTime, and lags-compatible access are exposed. + method_parity: setWindow, computeHistory/compute_history, structure round-trip, + and CovColl-producing history workflows are now implemented for single trains, + train collections, and trial history use. + defaults_parity: Window-boundary defaults are close to MATLAB for the implemented + history workflows. + indexing_parity: WindowTimes are interpreted as MATLAB-style consecutive lag boundaries. + error_warning_parity: Core validation is present, though MATLAB warning text and + some malformed-input branches remain thinner. + output_type_parity: Returns CovariateCollection outputs in the MATLAB-facing workflows + that consume History objects. + known_remaining_differences: + - Plotting and some specialized history-basis utilities remain unported. + required_remediation: + - Add MATLAB-derived fixtures for history-window outputs and multi-neuron history + collections. + plotting_report_parity: No dedicated history plotting parity beyond workflow-generated + covariates and notebook figures. +- matlab_name: Events + kind: class + matlab_path: Events.m + python_public_name: nstat.Events + python_impl_path: nstat/events.py + status: high_fidelity + constructor_parity: Constructor now tracks MATLAB eventTimes, eventLabels, and eventColor + semantics, including label-count validation. + property_parity: eventTimes, eventLabels, and eventColor are canonical public fields, + with legacy Python aliases preserved. + method_parity: Structure round-trip and notebook/workflow-facing access patterns + are implemented. + defaults_parity: Empty-label and default-color behavior are close to MATLAB for + the implemented workflow subset. + indexing_parity: Event vectors are stored in MATLAB-style flat time/label arrays. + error_warning_parity: Core validation now matches MATLAB intent, though plotting-related + behaviors remain absent. + output_type_parity: Returns canonical Events objects. + known_remaining_differences: + - Plotting and some MATLAB-specific display behaviors are still unported. + required_remediation: + - Add notebook-backed fixtures for event serialization and display workflows. + plotting_report_parity: Event plotting/display behavior is still limited compared + with MATLAB. +- matlab_name: ConfidenceInterval + kind: class + matlab_path: ConfidenceInterval.m + python_public_name: nstat.ConfidenceInterval + python_impl_path: nstat/confidence_interval.py + status: high_fidelity + constructor_parity: Basic time-and-bounds construction aligns with MATLAB intent. + property_parity: lower and upper accessors plus color metadata are exposed. + method_parity: Color assignment, plotting, and arithmetic composition with scalar + signals and other confidence intervals are implemented for the MATLAB-facing workflows + used by Covariate. + defaults_parity: Default color and time/bounds normalization are close to MATLAB. + indexing_parity: Bounds are stored in MATLAB-style n x 2 lower/upper form. + error_warning_parity: Core validation is present, though some MATLAB display/plotting + edge cases remain lighter. + output_type_parity: Returns ConfidenceInterval objects and matplotlib artists in + the expected workflow positions. + known_remaining_differences: + - Full MATLAB serialization/display semantics remain lighter than the original toolbox. + required_remediation: + - Add MATLAB-derived fixtures for serialized confidence-interval payloads and plot + styling. + plotting_report_parity: Core CI plotting works; full MATLAB display/serialization + styling remains lighter. +- matlab_name: CovColl + kind: class + matlab_path: CovColl.m + python_public_name: nstat.CovColl + python_impl_path: nstat/trial.py + status: high_fidelity + constructor_parity: CovColl now supports MATLAB-style direct construction, empty + initialization, and nested collection ingestion. + property_parity: Core collection state exists, including covArray, covDimensions, + numCov, minTime, maxTime, covMask, covShift, sampleRate, and original timing metadata. + method_parity: MATLAB-facing collection methods are now first-class, covering add/remove, + name/index lookup, mask selectors, time-window restriction, resampling, matrixWithTime, + dataToMatrix, shift/reset, label extraction, and restoreToOriginal. + defaults_parity: Default mask, shift, sample-rate, and timing behavior now track + MATLAB collection semantics closely. + indexing_parity: Shared-time enforcement is implemented. + error_warning_parity: Core validation is present, though some MATLAB warning text + and malformed-selector branches are still thinner. + output_type_parity: Returns Covariate and CovariateCollection-compatible outputs + across MATLAB-facing workflows. + known_remaining_differences: + - Some structure serialization and rarely used helper methods remain unported. + required_remediation: + - Add MATLAB-derived fixtures for selector masks, time-window coercion, and serialized + collection state. + plotting_report_parity: Collection plotting is available for core workflows; some + MATLAB summary visuals remain absent. +- matlab_name: getPaperDataDirs + kind: function + matlab_path: getPaperDataDirs.m + python_public_name: nstat.getPaperDataDirs + python_impl_path: nstat/data_manager.py + status: high_fidelity + constructor_parity: N/A + property_parity: N/A + method_parity: Python helper exposes MATLAB-style name and standalone repo semantics. + defaults_parity: Defaults to the Python repo's independent example-data cache instead + of a MATLAB checkout path. + indexing_parity: N/A + error_warning_parity: Close for the Python use case. + output_type_parity: Returns directory paths as a Python tuple/list structure rather + than MATLAB cell arrays. + known_remaining_differences: + - Python returns native path types/strings rather than MATLAB cells. + required_remediation: + - Add a MATLAB-reference fixture for the directory tuple shape if stricter parity + is needed. + plotting_report_parity: N/A +- matlab_name: nSTAT_Install + kind: function + matlab_path: nSTAT_Install.m + python_public_name: nstat.nSTAT_Install + python_impl_path: nstat/install.py + status: high_fidelity + constructor_parity: N/A + property_parity: N/A + method_parity: Python installer covers data download, docs rebuild, and MATLAB-compatible + flags while explicitly documenting the Python-only no-op path-preference behavior. + defaults_parity: Defaults are aligned to standalone Python packaging while preserving + MATLAB-facing flag names where reasonable. + indexing_parity: N/A + error_warning_parity: Installer status output and failure reporting are validated + in Python, with MATLAB path warnings intentionally replaced by structured Python + notes. + output_type_parity: Returns Python dictionaries/status text rather than MATLAB console-only + behavior. + known_remaining_differences: + - MATLAB path management is intentionally non-applicable in Python. + required_remediation: + - Keep documenting the no-op compatibility behavior and test installer status outputs. + plotting_report_parity: N/A +- matlab_name: nstatOpenHelpPage + kind: function + matlab_path: nstatOpenHelpPage.m + python_public_name: null + python_impl_path: null + status: not_applicable + constructor_parity: N/A + property_parity: N/A + method_parity: MATLAB help-browser integration has no direct standalone Python equivalent. + defaults_parity: N/A + indexing_parity: N/A + error_warning_parity: N/A + output_type_parity: N/A + known_remaining_differences: + - Python uses Sphinx docs pages instead of the MATLAB help browser. + required_remediation: + - None. + plotting_report_parity: N/A diff --git a/parity/notebook_fidelity.yml b/parity/notebook_fidelity.yml index 3355412b..3ee5062a 100644 --- a/parity/notebook_fidelity.yml +++ b/parity/notebook_fidelity.yml @@ -8,13 +8,18 @@ items: - topic: nSTATPaperExamples source_matlab: nSTATPaperExamples.mlx python_notebook: notebooks/nSTATPaperExamples.ipynb - fidelity_status: high_fidelity + fidelity_status: partial remaining_differences: Python uses standalone figshare-backed data access and generated - gallery assets rather than MATLAB path-based setup. + gallery assets rather than MATLAB path-based setup, and several sections still + rely on placeholder or tracker-only cells instead of full MATLAB-equivalent computations. python_sections: 31 python_expected_figures: 25 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 14 + python_tracker_only_cells: 5 + python_contains_placeholders: true + python_contains_tracker_only_cells: true matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 37 matlab_published_figures: 26 @@ -24,27 +29,38 @@ items: source_matlab: TrialExamples.mlx python_notebook: notebooks/TrialExamples.ipynb fidelity_status: high_fidelity - remaining_differences: Some MATLAB plotting/display details remain simplified, but - the core Trial object workflow now follows the MATLAB semantics closely. - python_sections: 3 + remaining_differences: The notebook now mirrors the MATLAB Trial workflow with executable + object construction, masking, history extraction, and plotting; the closing analysis + section uses one representative Python `Analysis` run instead of linking out to + separate MATLAB help pages. + python_sections: 9 python_expected_figures: 6 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 9 matlab_published_figures: 6 - section_delta: -6 + section_delta: 0 figure_delta: 0 - topic: AnalysisExamples source_matlab: AnalysisExamples.mlx python_notebook: notebooks/AnalysisExamples.ipynb - fidelity_status: high_fidelity - remaining_differences: Advanced MATLAB algorithm-selection branches and some report - plots are still lighter in Python. + fidelity_status: partial + remaining_differences: Advanced MATLAB algorithm-selection branches and report plots + remain lighter in Python, and the notebook still contains tracker-only visualization + sections rather than a fully executable MATLAB-equivalent workflow. python_sections: 2 python_expected_figures: 4 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 7 matlab_published_figures: 4 @@ -61,6 +77,10 @@ items: python_expected_figures: 5 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 4 matlab_published_figures: 5 @@ -77,6 +97,10 @@ items: python_expected_figures: 2 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 2 matlab_published_figures: 2 @@ -94,6 +118,10 @@ items: python_expected_figures: 9 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 7 matlab_published_figures: 9 @@ -111,6 +139,10 @@ items: python_expected_figures: 11 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 5 matlab_published_figures: 11 @@ -128,6 +160,10 @@ items: python_expected_figures: 3 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 6 matlab_published_figures: 3 @@ -136,13 +172,18 @@ items: - topic: PPSimExample source_matlab: PPSimExample.mlx python_notebook: notebooks/PPSimExample.ipynb - fidelity_status: high_fidelity - remaining_differences: MATLAB plotting/report formatting remains lighter, but the - core point-process simulation workflow is closely aligned. + fidelity_status: partial + remaining_differences: The notebook now executes the full Python point-process simulation + and analysis workflow without placeholders, but it still uses the native `CIFModel` + path rather than the original MATLAB/Simulink recursive CIF model. python_sections: 9 python_expected_figures: 3 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 17 matlab_published_figures: 8 @@ -160,6 +201,10 @@ items: python_expected_figures: 10 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 11 matlab_published_figures: 10 @@ -177,6 +222,10 @@ items: python_expected_figures: 6 python_uses_figure_tracker: true python_has_finalize_call: true + python_placeholder_cells: 0 + python_tracker_only_cells: 0 + python_contains_placeholders: false + python_contains_tracker_only_cells: false matlab_repo_root: /Users/iahncajigas/Library/CloudStorage/Dropbox/Codex/nSTAT matlab_sections: 4 matlab_published_figures: 6 diff --git a/parity/report.md b/parity/report.md index 10f4aa9f..0b3c5475 100644 --- a/parity/report.md +++ b/parity/report.md @@ -23,9 +23,9 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/no | Status | Count | |---|---:| | `exact` | 0 | -| `high_fidelity` | 18 | -| `partial` | 0 | -| `shim_only` | 0 | +| `high_fidelity` | 13 | +| `partial` | 5 | +| `wrapper_only` | 0 | | `missing` | 0 | | `not_applicable` | 1 | @@ -34,16 +34,29 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/no | Status | Count | |---|---:| | `exact` | 0 | -| `high_fidelity` | 11 | -| `partial` | 0 | +| `high_fidelity` | 8 | +| `partial` | 3 | + +## Simulink Fidelity Summary + +| Strategy | Count | +|---|---:| +| `native_python` | 1 | +| `generated_code_wrapped` | 0 | +| `packaged_runtime` | 0 | +| `matlab_engine_fallback` | 1 | +| `unsupported` | 0 | +| `reference_only` | 4 | ## Coverage Notes - Public API: no missing MATLAB public APIs remain; only the MATLAB help-browser utility is explicitly non-applicable. - Help/notebook parity: all inventoried MATLAB help workflows are mapped to Python notebooks or equivalents. -- Notebook fidelity: all tracked MATLAB-helpfile notebook ports are marked high fidelity or exact. +- Notebook fidelity: workflow coverage is complete, but 3 MATLAB-helpfile notebook ports are still marked partial in `tools/notebooks/parity_notes.yml`. +- Notebook fidelity audit: structural section/figure comparisons plus placeholder/tracker-only cell detection are recorded in `parity/notebook_fidelity.yml`. - Paper examples and docs gallery: all canonical paper examples and committed gallery directories are mapped. -- Class fidelity: the class audit reports no partial, shim-only, or missing items. +- Class fidelity: mapping parity is ahead of semantic parity; the audit still reports partial fidelity for several MATLAB-facing classes and workflows. +- Simulink fidelity: 2 Simulink-backed assets still rely on partial, fallback, or unsupported Python execution paths. ## Remaining Mapping Deltas @@ -51,11 +64,22 @@ No partial or missing items remain in the mapping inventory. ## Remaining Notebook-Fidelity Deltas -No partial notebook-fidelity items remain in `tools/notebooks/parity_notes.yml`. +- `nSTATPaperExamples` -> `notebooks/nSTATPaperExamples.ipynb` [partial]: Python uses standalone figshare-backed data access and generated gallery assets rather than MATLAB path-based setup, and several sections still rely on placeholder or tracker-only cells instead of full MATLAB-equivalent computations. +- `AnalysisExamples` -> `notebooks/AnalysisExamples.ipynb` [partial]: Advanced MATLAB algorithm-selection branches and report plots remain lighter in Python, and the notebook still contains tracker-only visualization sections rather than a fully executable MATLAB-equivalent workflow. +- `PPSimExample` -> `notebooks/PPSimExample.ipynb` [partial]: The notebook now executes the full Python point-process simulation and analysis workflow without placeholders, but it still uses the native `CIFModel` path rather than the original MATLAB/Simulink recursive CIF model. ## Remaining Class-Fidelity Deltas -No partial, shim-only, or missing class-fidelity items remain. +- `Analysis` -> `nstat.Analysis` [partial]: Add dataset-backed numerical parity fixtures for canonical analysis workflows. +- `FitResult` -> `nstat.FitResult` [partial]: Add MATLAB-derived golden fixtures for coefficient metadata and validation/report payloads. +- `FitResSummary` -> `nstat.FitResSummary` [partial]: Add golden fixtures for multi-neuron summary aggregation and remaining report outputs. +- `CIF` -> `nstat.CIF` [partial]: Add MATLAB-derived fixtures for CIF evaluation and thinning outputs. +- `DecodingAlgorithms` -> `nstat.DecodingAlgorithms` [partial]: Add MATLAB-derived numerical fixtures for DecodingExample, DecodingExampleWithHist, StimulusDecode2D, and HybridFilterExample. + +## Simulink Fidelity Deltas + +- `PointProcessSimulation` -> `PointProcessSimulation.slx` [native_python/partial]: Native Python simulation through `nstat.cif` and `nstat.simulation`, with MATLAB/Simulink fixture comparison still pending. +- `SimulatedNetwork2` -> `helpfiles/SimulatedNetwork2.mdl` [matlab_engine_fallback/partial]: Prefer a future native Python reimplementation, but document MATLAB Engine fallback first because no faithful Python executable path exists yet. ## Justified Non-Applicable Items diff --git a/parity/simulink_fidelity.yml b/parity/simulink_fidelity.yml new file mode 100644 index 00000000..1749038c --- /dev/null +++ b/parity/simulink_fidelity.yml @@ -0,0 +1,83 @@ +version: 1 +generated_on: 2026-03-07 +source_repositories: + matlab: https://github.com/cajigaslab/nSTAT + python: https://github.com/cajigaslab/nSTAT-python +strategy_legend: + - native_python + - generated_code_wrapped + - packaged_runtime + - matlab_engine_fallback + - unsupported + - reference_only +items: + - model_name: PointProcessSimulation + model_path: PointProcessSimulation.slx + purpose: Discrete point-process simulation used by `CIF.simulateCIF`, `PPSimExample`, and related help workflows. + matlab_usage: required_for_behavioral_parity + python_strategy: native_python + current_python_status: partial + chosen_interoperability_strategy: Native Python simulation through `nstat.cif` and `nstat.simulation`, with MATLAB/Simulink fixture comparison still pending. + fidelity_risks: + - Simulink block timing and solver semantics are not yet fixture-checked against the Python path. + - MATLAB supports explicit binomial-versus-poisson model branching inside the model; Python approximates the executed path analytically. + validation_plan: + - Compare fixed-seed spike-count statistics and lambda traces against MATLAB/Simulink reference runs. + - Add a PPSimExample regression fixture that exercises the same stimulus and history filters. + - model_name: PointProcessSimulationCont + model_path: PointProcessSimulationCont.slx + purpose: Continuous-time companion model kept with the MATLAB toolbox for simulation/reference work. + matlab_usage: used_for_reference + python_strategy: reference_only + current_python_status: reference_only + chosen_interoperability_strategy: Keep as reference while the Python port uses the native discrete simulation path. + fidelity_risks: + - No Python-executable equivalent currently mirrors the continuous Simulink model directly. + validation_plan: + - Audit whether any MATLAB helpfile or paper workflow depends on the continuous model before promoting it beyond reference-only. + - model_name: PointProcessSimulationCache + model_path: PointProcessSimulation.slxc + purpose: Simulink compiled cache artifact for `PointProcessSimulation`. + matlab_usage: used_for_reference + python_strategy: reference_only + current_python_status: reference_only + chosen_interoperability_strategy: Treat as a MATLAB build artifact, not as a Python execution target. + fidelity_risks: + - Cache files are version-specific and not portable across environments. + validation_plan: + - None beyond confirming the source `.slx` model remains inventoried. + - model_name: HelpPointProcessSimulationCache + model_path: helpfiles/PointProcessSimulation.slxc + purpose: Published-help compiled cache artifact for the point-process simulation model. + matlab_usage: used_for_reference + python_strategy: reference_only + current_python_status: reference_only + chosen_interoperability_strategy: Treat as a published-help artifact only. + fidelity_risks: + - Not executable from Python and tied to MATLAB's published help pipeline. + validation_plan: + - None beyond inventory coverage. + - model_name: SimulatedNetwork2 + model_path: helpfiles/SimulatedNetwork2.mdl + purpose: Two-neuron network simulation used by `NetworkTutorial` and related connectivity examples. + matlab_usage: required_for_example_execution + python_strategy: matlab_engine_fallback + current_python_status: partial + chosen_interoperability_strategy: Prefer a future native Python reimplementation, but document MATLAB Engine fallback first because no faithful Python executable path exists yet. + fidelity_risks: + - One-sample delays and block-level binomial firing semantics may not match a naive Python rewrite. + - The current Python notebook does not yet execute the original Simulink model. + validation_plan: + - Add a MATLAB Engine smoke path for environments that provide MATLAB. + - Capture reference outputs from the model before attempting a native Python port. + - model_name: SimulatedNetwork2Cache + model_path: helpfiles/SimulatedNetwork2.slxc + purpose: Simulink compiled cache artifact for `SimulatedNetwork2`. + matlab_usage: used_for_reference + python_strategy: reference_only + current_python_status: reference_only + chosen_interoperability_strategy: Treat as a MATLAB build artifact, not a Python target. + fidelity_risks: + - Cache files are version-specific and cannot serve as a stable Python execution path. + validation_plan: + - None beyond inventory coverage. diff --git a/tests/test_class_fidelity_audit.py b/tests/test_class_fidelity_audit.py index c8dffa60..264ecb74 100644 --- a/tests/test_class_fidelity_audit.py +++ b/tests/test_class_fidelity_audit.py @@ -7,7 +7,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] AUDIT_PATH = REPO_ROOT / "parity" / "class_fidelity.yml" -VALID_STATUSES = {"exact", "high_fidelity", "partial", "shim_only", "missing", "not_applicable"} +VALID_STATUSES = {"exact", "high_fidelity", "partial", "wrapper_only", "missing", "not_applicable"} PRIORITY_CLASSES = { "SignalObj", "Covariate", @@ -44,17 +44,38 @@ def test_class_fidelity_audit_uses_known_status_values() -> None: assert item["status"] in VALID_STATUSES -def test_core_matlab_facing_classes_are_not_shim_only() -> None: +def test_core_matlab_facing_classes_are_not_wrapper_only() -> None: payload = _load_audit() audit_by_name = {str(item["matlab_name"]): item for item in payload["items"]} for name in ("SignalObj", "Covariate", "nspikeTrain"): row = audit_by_name[name] - assert row["status"] not in {"shim_only", "missing"} - assert row["python_path"] == "nstat/core.py" + assert row["status"] not in {"wrapper_only", "missing"} + assert row["python_impl_path"] == "nstat/core.py" def test_class_fidelity_audit_has_unique_matlab_names() -> None: payload = _load_audit() names = [str(item.get("matlab_name", "")).strip() for item in payload["items"]] assert len(names) == len(set(names)) + + +def test_class_fidelity_audit_uses_requested_field_names() -> None: + payload = _load_audit() + required = { + "matlab_name", + "matlab_path", + "python_public_name", + "python_impl_path", + "status", + "constructor_parity", + "property_parity", + "method_parity", + "defaults_parity", + "indexing_parity", + "plotting_report_parity", + "known_remaining_differences", + "required_remediation", + } + for item in payload["items"]: + assert required <= set(item), f"Missing required class-fidelity fields for {item.get('matlab_name')}" diff --git a/tests/test_fitresult_diagnostics.py b/tests/test_fitresult_diagnostics.py new file mode 100644 index 00000000..3e88a04b --- /dev/null +++ b/tests/test_fitresult_diagnostics.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import matplotlib.pyplot as plt +import numpy as np + +from nstat import Analysis, CIFModel, ConfigCollection, Covariate, CovariateCollection, FitSummary, Trial, TrialConfig + + +def _build_fit_result(): + t = np.arange(0.0, 1.0, 0.001) + stim = np.sin(2 * np.pi * 2 * t) + cov = Covariate(t, stim, "stim", "time", "s", "a.u.", ["stim"]) + + model = CIFModel(t, 12.0 + 4.0 * np.maximum(stim, 0.0), name="lambda") + spikes = model.simulate(num_realizations=2, seed=7) + + trial = Trial(spike_collection=spikes, covariate_collection=CovariateCollection([cov])) + cfgs = ConfigCollection([TrialConfig(covMask=["stim"], sampleRate=1000.0, name="stim_model")]) + return Analysis.run_analysis_for_all_neurons(trial, cfgs)[0] + + +def test_fitresult_diagnostics_populate_ks_and_residual_fields() -> None: + fit = _build_fit_result() + + ks = fit.computeKSStats() + residual = fit.computeFitResidual() + inv = fit.computeInvGausTrans() + + assert "ks_stat" in ks + assert fit.KSStats.shape == (1, 1) + assert residual.name == "fit residual" + assert np.asarray(inv, dtype=float).ndim == 1 + assert fit.Residual is not None + + +def test_fitresult_plotting_methods_return_matplotlib_objects() -> None: + fit = _build_fit_result() + + fig = fit.plotResults() + ax1 = fit.KSPlot() + ax2 = fit.plotResidual() + ax3 = fit.plotInvGausTrans() + ax4 = fit.plotSeqCorr() + ax5 = fit.plotCoeffs() + + assert len(fig.axes) == 4 + for ax in (ax1, ax2, ax3, ax4, ax5): + assert hasattr(ax, "plot") + plt.close("all") + + +def test_fitsummary_plotsummary_returns_figure() -> None: + fit = _build_fit_result() + summary = FitSummary([fit]) + fig = summary.plotSummary() + assert len(fig.axes) == 3 + plt.close("all") diff --git a/tests/test_notebook_ci_groups.py b/tests/test_notebook_ci_groups.py index 85ead1ac..7149df43 100644 --- a/tests/test_notebook_ci_groups.py +++ b/tests/test_notebook_ci_groups.py @@ -11,7 +11,6 @@ REQUIRED_CI_SMOKE_TOPICS = { "ConfidenceIntervalOverview", - "nSTATPaperExamples", } REQUIRED_PARITY_CORE_TOPICS = { "AnalysisExamples", @@ -28,6 +27,19 @@ "nSTATPaperExamples", "nSpikeTrainExamples", } +REQUIRED_HELPFILE_FULL_TOPICS = { + "AnalysisExamples", + "DecodingExample", + "DecodingExampleWithHist", + "ExplicitStimulusWhiskerData", + "HippocampalPlaceCellExample", + "HybridFilterExample", + "PPSimExample", + "StimulusDecode2D", + "TrialExamples", + "ValidationDataSet", + "nSTATPaperExamples", +} def test_ci_smoke_group_covers_required_parity_notebooks() -> None: @@ -81,3 +93,14 @@ def test_parity_core_group_topics_exist_in_notebook_manifest() -> None: missing = [topic for topic in parity_core if topic not in notebook_topics] assert not missing, f"parity_core group references unknown notebook topics: {missing}" + + +def test_helpfile_full_group_covers_all_tracked_helpfile_ports() -> None: + notebook_manifest = yaml.safe_load(NOTEBOOK_MANIFEST_PATH.read_text(encoding="utf-8")) or {} + notebook_topics = {row["topic"] for row in notebook_manifest.get("notebooks", [])} + + groups_payload = yaml.safe_load(TOPIC_GROUPS_PATH.read_text(encoding="utf-8")) or {} + helpfile_full = set(groups_payload.get("groups", {}).get("helpfile_full", [])) + + assert REQUIRED_HELPFILE_FULL_TOPICS <= notebook_topics + assert REQUIRED_HELPFILE_FULL_TOPICS == helpfile_full diff --git a/tests/test_notebook_fidelity_audit.py b/tests/test_notebook_fidelity_audit.py index e3a5a658..b08ec186 100644 --- a/tests/test_notebook_fidelity_audit.py +++ b/tests/test_notebook_fidelity_audit.py @@ -29,11 +29,23 @@ def test_notebook_fidelity_audit_has_structural_counts() -> None: assert "python_expected_figures" in row assert row["python_expected_figures"] >= 0 assert isinstance(row["python_has_finalize_call"], bool) + assert "python_placeholder_cells" in row + assert "python_tracker_only_cells" in row -def test_notebook_fidelity_audit_has_no_partial_items() -> None: +def test_notebook_fidelity_audit_marks_placeholder_heavy_ports_as_partial() -> None: audit = yaml.safe_load(AUDIT_PATH.read_text(encoding="utf-8")) or {} - assert all(row["fidelity_status"] != "partial" for row in audit.get("items", [])) + partial_topics = {row["topic"] for row in audit.get("items", []) if row["fidelity_status"] == "partial"} + assert {"AnalysisExamples", "PPSimExample", "nSTATPaperExamples"} <= partial_topics + + +def test_high_fidelity_notebooks_have_no_placeholder_or_tracker_only_cells() -> None: + audit = yaml.safe_load(AUDIT_PATH.read_text(encoding="utf-8")) or {} + for row in audit.get("items", []): + if row["fidelity_status"] not in {"high_fidelity", "exact"}: + continue + assert not row["python_contains_placeholders"], f"{row['topic']} still contains placeholder code" + assert not row["python_contains_tracker_only_cells"], f"{row['topic']} still contains tracker-only cells" def test_notebook_fidelity_audit_matches_generator_when_matlab_repo_is_available() -> None: diff --git a/tests/test_notebook_parity_notes.py b/tests/test_notebook_parity_notes.py index 9b62c9fe..75c7b0d9 100644 --- a/tests/test_notebook_parity_notes.py +++ b/tests/test_notebook_parity_notes.py @@ -39,4 +39,5 @@ def test_target_notebooks_start_with_machine_readable_parity_note() -> None: def test_notebook_parity_notes_have_no_partial_statuses() -> None: - assert all(row["fidelity_status"] != "partial" for row in _load_notes()) + partial = [row["topic"] for row in _load_notes() if row["fidelity_status"] == "partial"] + assert partial, "The current audit should include at least one partial notebook until placeholder-heavy ports are replaced" diff --git a/tests/test_parity_report.py b/tests/test_parity_report.py index 39f400f6..a11cf686 100644 --- a/tests/test_parity_report.py +++ b/tests/test_parity_report.py @@ -21,10 +21,13 @@ def test_parity_report_highlights_current_constraints() -> None: assert "all canonical paper examples and committed gallery directories are mapped" in text assert "class fidelity" in text.lower() assert "Notebook Fidelity Summary" in text + assert "Simulink Fidelity Summary" in text assert "Remaining Notebook-Fidelity Deltas" in text - assert "all tracked MATLAB-helpfile notebook ports are marked high fidelity or exact" in text - assert "No partial notebook-fidelity items remain in `tools/notebooks/parity_notes.yml`." in text + assert "MATLAB-helpfile notebook ports are still marked partial" in text + assert "AnalysisExamples" in text assert "No partial or missing items remain in the mapping inventory." in text assert "Remaining Class-Fidelity Deltas" in text - assert "No partial, shim-only, or missing class-fidelity items remain." in text + assert "partial fidelity for several MATLAB-facing classes and workflows" in text + assert "Simulink Fidelity Deltas" in text + assert "PointProcessSimulation" in text assert "nstatOpenHelpPage" in text diff --git a/tests/test_simulink_fidelity_audit.py b/tests/test_simulink_fidelity_audit.py new file mode 100644 index 00000000..32c3fcc0 --- /dev/null +++ b/tests/test_simulink_fidelity_audit.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +AUDIT_PATH = REPO_ROOT / "parity" / "simulink_fidelity.yml" +MATLAB_REPO_ROOT = REPO_ROOT.parent / "nSTAT" +VALID_STRATEGIES = { + "native_python", + "generated_code_wrapped", + "packaged_runtime", + "matlab_engine_fallback", + "unsupported", + "reference_only", +} + + +def _load_audit() -> dict: + payload = yaml.safe_load(AUDIT_PATH.read_text(encoding="utf-8")) or {} + assert payload.get("items"), "parity/simulink_fidelity.yml is empty" + return payload + + +def test_simulink_fidelity_audit_uses_known_strategy_values() -> None: + payload = _load_audit() + for row in payload["items"]: + assert row["python_strategy"] in VALID_STRATEGIES + + +def test_simulink_fidelity_audit_records_required_execution_fields() -> None: + payload = _load_audit() + for row in payload["items"]: + assert row["model_path"] + assert row["purpose"] + assert row["chosen_interoperability_strategy"] + assert row["validation_plan"] + + +def test_simulink_fidelity_audit_paths_exist_when_matlab_repo_is_available() -> None: + if not MATLAB_REPO_ROOT.exists(): + pytest.skip(f"MATLAB reference repo not available at {MATLAB_REPO_ROOT}") + + payload = _load_audit() + missing = [row["model_path"] for row in payload["items"] if not (MATLAB_REPO_ROOT / row["model_path"]).exists()] + assert not missing, f"Missing Simulink audit paths in MATLAB repo: {missing}" diff --git a/tests/test_trial_plotting_surface.py b/tests/test_trial_plotting_surface.py new file mode 100644 index 00000000..fbf5c104 --- /dev/null +++ b/tests/test_trial_plotting_surface.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import matplotlib.pyplot as plt +import numpy as np + +from nstat import CovColl, Covariate, Events, History, Trial, nspikeTrain, nstColl + + +def test_history_and_events_plot_surface_returns_axes() -> None: + hist = History([0.0, 0.1, 0.2, 0.4]) + events = Events([0.2, 0.7], ["E1", "E2"]) + + ax_hist = hist.plot() + ax_events = events.plot() + + assert hasattr(ax_hist, "broken_barh") + assert hasattr(ax_events, "vlines") + plt.close("all") + + +def test_covcoll_nstcoll_and_trial_plot_surface_return_matplotlib_objects() -> None: + t = np.linspace(0.0, 1.0, 1001) + position = Covariate(t, np.column_stack([np.sin(2 * np.pi * t), np.cos(2 * np.pi * t)]), "Position", "time", "s", "a.u.", ["x", "y"]) + force = Covariate(t, np.column_stack([np.cos(4 * np.pi * t), np.sin(4 * np.pi * t)]), "Force", "time", "s", "a.u.", ["f_x", "f_y"]) + covs = CovColl([position, force]) + + spikes = nstColl( + [ + nspikeTrain([0.1, 0.2, 0.5, 0.8], name="1", minTime=0.0, maxTime=1.0, makePlots=-1), + nspikeTrain([0.15, 0.25, 0.45, 0.9], name="2", minTime=0.0, maxTime=1.0, makePlots=-1), + ] + ) + trial = Trial(spikes, covs, Events([0.3], ["cue"]), History([0.0, 0.05, 0.1])) + + fig_cov = covs.plot() + ax_spikes = spikes.plot() + fig_trial = trial.plot() + + assert len(fig_cov.axes) == 2 + assert hasattr(ax_spikes, "vlines") + assert len(fig_trial.axes) == 4 + plt.close("all") diff --git a/tools/notebooks/build_foundational_help_notebooks.py b/tools/notebooks/build_foundational_help_notebooks.py new file mode 100644 index 00000000..12b05494 --- /dev/null +++ b/tools/notebooks/build_foundational_help_notebooks.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import nbformat +from nbformat.v4 import new_code_cell, new_markdown_cell, new_notebook + + +REPO_ROOT = Path(__file__).resolve().parents[2] +NOTEBOOK_DIR = REPO_ROOT / "notebooks" + + +LANGUAGE_METADATA = { + "language_info": { + "name": "python", + } +} + + +def _write_notebook( + path: Path, + *, + topic: str, + expected_figures: int, + markdown_note: str, + code_cells: list[str], +) -> None: + notebook = new_notebook( + cells=[ + new_markdown_cell(markdown_note), + *[new_code_cell(dedent(cell).strip() + "\n") for cell in code_cells], + ], + metadata={ + **LANGUAGE_METADATA, + "nstat": { + "expected_figures": expected_figures, + "run_group": "smoke", + "style": "python-example", + "topic": topic, + }, + }, + ) + path.write_text(nbformat.writes(notebook), encoding="utf-8") + + +TRIAL_NOTE = """\ + +## MATLAB Parity Note +- Source MATLAB helpfile: `TrialExamples.mlx` +- Fidelity status: `high_fidelity` +- Remaining justified differences: The notebook now mirrors the MATLAB Trial workflow with executable object construction, masking, history extraction, and plotting; the closing analysis section uses one representative Python `Analysis` run instead of linking out to separate MATLAB help pages. +""" + + +TRIAL_CODE = [ + """ + # nSTAT-python notebook example: TrialExamples + from pathlib import Path + import sys + + REPO_ROOT = Path.cwd().resolve().parent + if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + SRC_PATH = (REPO_ROOT / "src").resolve() + if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + import numpy as np + + from nstat import Analysis, ConfigColl, CovColl, Covariate, Events, History, Trial, TrialConfig, nspikeTrain, nstColl + from nstat.notebook_figures import FigureTracker + + np.random.seed(7) + OUTPUT_ROOT = REPO_ROOT / "output" / "notebook_images" + __tracker = FigureTracker(topic='TrialExamples', output_root=OUTPUT_ROOT, expected_count=6) + + + def _figure(label: str, *, figsize=(8.5, 3.5)): + fig = __tracker.new_figure(label) + fig.clear() + fig.set_size_inches(*figsize) + return fig + + + def _build_trial(): + length_trial = 1.0 + sample_rate = 1000.0 + time = np.linspace(0.0, length_trial, int(length_trial * sample_rate) + 1) + + position = Covariate( + time, + np.column_stack([np.sin(2 * np.pi * time), np.cos(2 * np.pi * time)]), + "Position", + "time", + "s", + "a.u.", + ["x", "y"], + ) + force = Covariate( + time, + np.column_stack( + [ + 0.5 * np.cos(4 * np.pi * time) + 0.15 * np.sin(2 * np.pi * time), + 0.4 * np.sin(4 * np.pi * time + 0.3), + ] + ), + "Force", + "time", + "s", + "N", + ["f_x", "f_y"], + ) + cov_coll = CovColl([position, force]) + cov_coll.setMaxTime(length_trial) + + events = Events([0.18, 0.72], ["E_1", "E_2"]) + history = History([0.0, 0.1, 0.2, 0.4]) + + trains = [] + base_grid = np.linspace(0.05, 0.95, 100) + for neuron_index, phase in enumerate(np.linspace(0.0, np.pi / 2.0, 4), start=1): + spikes = np.clip(base_grid + 0.008 * np.sin(2 * np.pi * base_grid * (neuron_index + 1) + phase), 0.0, length_trial) + trains.append( + nspikeTrain( + np.sort(spikes), + name=str(neuron_index), + minTime=0.0, + maxTime=length_trial, + makePlots=-1, + ) + ) + spike_coll = nstColl(trains) + trial = Trial(spike_coll, cov_coll, events, history) + return { + "length_trial": length_trial, + "sample_rate": sample_rate, + "history": history, + "cov_coll": cov_coll, + "events": events, + "spike_coll": spike_coll, + "trial": trial, + } + + + ctx = _build_trial() + print( + { + "trial_duration_s": ctx["length_trial"], + "num_neurons": ctx["spike_coll"].numSpikeTrains, + "covariates": ctx["cov_coll"].names, + "history_windows": ctx["history"].windowTimes.tolist(), + } + ) + """, + """ + # SECTION 1: Example 1: A simple data set + plt.close("all") + trial1 = ctx["trial"] + spikeColl = ctx["spike_coll"] + cc = ctx["cov_coll"] + e = ctx["events"] + h = ctx["history"] + """, + """ + # SECTION 2: Create History windows of interest + fig = _figure("figure; h.plot", figsize=(8.0, 2.5)) + ax = fig.subplots(1, 1) + h.plot(handle=ax) + """, + """ + # SECTION 3: Load Covariates + fig = _figure("figure; cc.plot", figsize=(8.5, 5.0)) + cc.plot(handle=fig) + """, + """ + # SECTION 4: Create trial events + fig = _figure("figure; e.plot", figsize=(8.0, 2.3)) + ax = fig.subplots(1, 1) + e.plot(handle=ax) + """, + """ + # SECTION 5: Create neural Spike Train Data + fig = _figure("figure; spikeColl.plot", figsize=(8.5, 3.5)) + ax = fig.subplots(1, 1) + spikeColl.plot(handle=ax) + """, + """ + # SECTION 6: Finally we have everything we need to create a Trial object. + fig = _figure("figure; trial1.plot", figsize=(9.0, 8.0)) + trial1.plot(handle=fig) + """, + """ + # SECTION 7: Mask out some of the data and plot the trial once again + trial1.setCovMask([["Position", "x"], ["Force", "f_x"]]) + fig = _figure("figure; trial1.plot masked", figsize=(9.0, 8.0)) + trial1.plot(handle=fig) + hist_cov = trial1.getHistForNeurons([1, 2]) + print({"masked_labels": trial1.getLabelsFromMask(1), "history_covariates": hist_cov.getAllCovLabels()[:4]}) + trial1.resetCovMask() + """, + """ + # SECTION 8: Example 2: Analyzing Trial Data + cfg = TrialConfig([["Position", "x"], ["Force", "f_x"]], sampleRate=ctx["sample_rate"], history=[0.0, 0.05, 0.1], name="Position+Force+History") + cfgColl = ConfigColl([cfg]) + fit = Analysis.RunAnalysisForNeuron(trial1, 1, cfgColl) + fit_stats = fit.computeKSStats() + print( + { + "config_name": fit.configNames[0], + "aic": round(float(fit.AIC[0]), 3), + "bic": round(float(fit.BIC[0]), 3), + "ks_stat": round(float(fit_stats["ks_stat"]), 4), + } + ) + """, + """ + # SECTION 9: Related analysis workflows + print("For larger model-comparison walkthroughs, continue with AnalysisExamples and AnalysisExamples2.") + __tracker.finalize() + """, +] + + +PPSIM_NOTE = """\ + +## MATLAB Parity Note +- Source MATLAB helpfile: `PPSimExample.mlx` +- Fidelity status: `partial` +- Remaining justified differences: The notebook now executes the full Python point-process simulation and analysis workflow without placeholders, but it still uses the native `CIFModel` path rather than the original MATLAB/Simulink recursive CIF model. +""" + + +PPSIM_CODE = [ + """ + # nSTAT-python notebook example: PPSimExample + from pathlib import Path + import sys + + REPO_ROOT = Path.cwd().resolve().parent + if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + SRC_PATH = (REPO_ROOT / "src").resolve() + if str(SRC_PATH) not in sys.path: + sys.path.insert(0, str(SRC_PATH)) + + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + import numpy as np + + from nstat import Analysis, CIFModel, ConfigColl, CovColl, Covariate, FitResSummary, Trial, TrialConfig + from nstat.notebook_figures import FigureTracker + + np.random.seed(5) + OUTPUT_ROOT = REPO_ROOT / "output" / "notebook_images" + __tracker = FigureTracker(topic='PPSimExample', output_root=OUTPUT_ROOT, expected_count=3) + + + def _figure(label: str, *, figsize=(8.5, 4.5)): + fig = __tracker.new_figure(label) + fig.clear() + fig.set_size_inches(*figsize) + return fig + + + def _logistic_rate(time, stimulus, mu=-3.0): + dt = float(np.median(np.diff(time))) + eta = mu + stimulus + p = np.exp(np.clip(eta, -20.0, 20.0)) + p = p / (1.0 + p) + return p / max(dt, 1e-12) + + + Ts = 0.001 + tMin = 0.0 + tMax = 10.0 + t = np.arange(tMin, tMax + Ts, Ts) + mu = -3.0 + stimulus_signal = np.sin(2 * np.pi * 1.0 * t) + baseline = Covariate(t, np.ones_like(t), "Baseline", "time", "s", "", ["mu"]) + stim = Covariate(t, stimulus_signal, "Stimulus", "time", "s", "Voltage", ["sin"]) + rate_hz = _logistic_rate(t, stimulus_signal, mu=mu) + lambda_model = CIFModel(t, rate_hz, name="lambda") + sC = lambda_model.simulate(num_realizations=5, seed=5) + cc = CovColl([stim, baseline]) + trial = Trial(sC, cc) + print({"duration_s": tMax, "num_realizations": sC.numSpikeTrains, "mean_rate_hz": round(float(np.mean(rate_hz)), 3)}) + """, + """ + # SECTION 1: General Point Process Simulation + plt.close("all") + """, + """ + # SECTION 2: Point Process Sample Path Generation + # This Python port uses a native CIFModel-driven rate simulation instead of the original MATLAB/Simulink model. + """, + """ + # SECTION 3: History Effect + selfHist = [0.0, 0.001, 0.002, 0.003] + print({"history_windows_s": selfHist}) + """, + """ + # SECTION 4: Stimulus Effect + print({"stimulus_frequency_hz": 1.0, "stimulus_amplitude": 1.0}) + """, + """ + # SECTION 5: Ensemble Effect + print({"ensemble_effect": 0.0}) + """, + """ + # SECTION 6: Generate sample paths + fig = _figure("figure; subplot(2,1,1); sC.plot; subplot(2,1,2); stim.plot", figsize=(10.0, 5.5)) + axs = fig.subplots(2, 1, sharex=True) + sC.plot(handle=axs[0]) + axs[0].set_xlim(0.0, tMax / 5.0) + stim.plot(handle=axs[1]) + axs[1].set_xlim(0.0, tMax / 5.0) + """, + """ + # SECTION 7: GLM Model Fitting Setup + cfg = [ + TrialConfig([["Baseline", "mu"]], sampleRate=1.0 / Ts, name="Baseline"), + TrialConfig([["Baseline", "mu"], ["Stimulus", "sin"]], sampleRate=1.0 / Ts, name="Stim"), + TrialConfig([["Baseline", "mu"], ["Stimulus", "sin"]], sampleRate=1.0 / Ts, history=selfHist, name="Stim+Hist"), + ] + cfgColl = ConfigColl(cfg) + """, + """ + # SECTION 8: GLM Model Fitting and Results + results = Analysis.RunAnalysisForAllNeurons(trial, cfgColl) + fig = _figure("results{1}.plotResults", figsize=(11.0, 8.0)) + results[0].plotResults(handle=fig) + """, + """ + # SECTION 9: Results for across all sample paths + summary = FitResSummary(results) + fig = _figure("Summary.plotSummary", figsize=(10.0, 4.5)) + summary.plotSummary(handle=fig) + print({"fit_names": summary.fitNames, "mean_AIC": np.asarray(summary.AIC, dtype=float).round(3).tolist()}) + __tracker.finalize() + """, +] + + +def main() -> int: + NOTEBOOK_DIR.mkdir(parents=True, exist_ok=True) + _write_notebook( + NOTEBOOK_DIR / "TrialExamples.ipynb", + topic="TrialExamples", + expected_figures=6, + markdown_note=TRIAL_NOTE, + code_cells=TRIAL_CODE, + ) + _write_notebook( + NOTEBOOK_DIR / "PPSimExample.ipynb", + topic="PPSimExample", + expected_figures=3, + markdown_note=PPSIM_NOTE, + code_cells=PPSIM_CODE, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/notebooks/build_helpfile_fidelity_notebooks.py b/tools/notebooks/build_helpfile_fidelity_notebooks.py index 89239f61..a490f1e0 100644 --- a/tools/notebooks/build_helpfile_fidelity_notebooks.py +++ b/tools/notebooks/build_helpfile_fidelity_notebooks.py @@ -416,13 +416,11 @@ def _plot_isi_hist(ax, train, lambda_hz, *, title): """ # SECTION 1: Case #1: Constant Rate Poisson Process # First we verify that the analysis recovers a constant Poisson rate from simulated spike trains. - pass """, """ # SECTION 2: Generate constant-rate neural firing activity constant_time = np.asarray(constant_case["time_s"], dtype=float) constant_trains = list(constant_case["trains"]) - pass """, """ # SECTION 3: Sanity check the ISI distribution @@ -466,7 +464,6 @@ def _plot_isi_hist(ax, train, lambda_hz, *, title): # Next we compare a single-rate model against a two-epoch rate model. piecewise_time = np.asarray(piecewise_case["time_s"], dtype=float) piecewise_trains = list(piecewise_case["trains"]) - pass """, """ # SECTION 7: Generate the piecewise-rate spike trains @@ -494,7 +491,6 @@ def _plot_isi_hist(ax, train, lambda_hz, *, title): """ # SECTION 8: Setup the piecewise-rate analysis piecewise_results = Analysis.RunAnalysisForAllNeurons(piecewise_case["trial"], piecewise_case["cfg"], 0) - pass """, """ # SECTION 9: Run the piecewise-rate analysis @@ -841,12 +837,10 @@ def _plot_raster(ax, time_s, spikes, *, max_cells=18): """ # SECTION 1: Problem Statement # We infer both a discrete movement state and a continuous reach trajectory from point-process observations. - pass """, """ # SECTION 2: Hybrid state-space setup # The Python port keeps the same two-state problem structure as MATLAB: a low-motion state and a movement state. - pass """, """ # SECTION 3: Generated Simulated Arm Reach @@ -896,7 +890,6 @@ def _plot_raster(ax, time_s, spikes, *, max_cells=18): """ # SECTION 4: Simulate Neural Firing # The simulated spike population depends on the latent state and the movement dynamics. - pass """, """ # SECTION 5: Run the hybrid filter diff --git a/tools/notebooks/parity_notes.yml b/tools/notebooks/parity_notes.yml index 73b6387d..912c3a48 100644 --- a/tools/notebooks/parity_notes.yml +++ b/tools/notebooks/parity_notes.yml @@ -3,18 +3,18 @@ notes: - topic: nSTATPaperExamples file: notebooks/nSTATPaperExamples.ipynb source_matlab: nSTATPaperExamples.mlx - fidelity_status: high_fidelity - remaining_differences: Python uses standalone figshare-backed data access and generated gallery assets rather than MATLAB path-based setup. + fidelity_status: partial + remaining_differences: Python uses standalone figshare-backed data access and generated gallery assets rather than MATLAB path-based setup, and several sections still rely on placeholder or tracker-only cells instead of full MATLAB-equivalent computations. - topic: TrialExamples file: notebooks/TrialExamples.ipynb source_matlab: TrialExamples.mlx fidelity_status: high_fidelity - remaining_differences: Some MATLAB plotting/display details remain simplified, but the core Trial object workflow now follows the MATLAB semantics closely. + remaining_differences: The notebook now mirrors the MATLAB Trial workflow with executable object construction, masking, history extraction, and plotting; the closing analysis section uses one representative Python `Analysis` run instead of linking out to separate MATLAB help pages. - topic: AnalysisExamples file: notebooks/AnalysisExamples.ipynb source_matlab: AnalysisExamples.mlx - fidelity_status: high_fidelity - remaining_differences: Advanced MATLAB algorithm-selection branches and some report plots are still lighter in Python. + fidelity_status: partial + remaining_differences: Advanced MATLAB algorithm-selection branches and report plots remain lighter in Python, and the notebook still contains tracker-only visualization sections rather than a fully executable MATLAB-equivalent workflow. - topic: DecodingExample file: notebooks/DecodingExample.ipynb source_matlab: DecodingExample.mlx @@ -43,8 +43,8 @@ notes: - topic: PPSimExample file: notebooks/PPSimExample.ipynb source_matlab: PPSimExample.mlx - fidelity_status: high_fidelity - remaining_differences: MATLAB plotting/report formatting remains lighter, but the core point-process simulation workflow is closely aligned. + fidelity_status: partial + remaining_differences: The notebook now executes the full Python point-process simulation and analysis workflow without placeholders, but it still uses the native `CIFModel` path rather than the original MATLAB/Simulink recursive CIF model. - topic: ValidationDataSet file: notebooks/ValidationDataSet.ipynb source_matlab: ValidationDataSet.mlx diff --git a/tools/notebooks/topic_groups.yml b/tools/notebooks/topic_groups.yml index 63f06cc2..22a8c1bf 100644 --- a/tools/notebooks/topic_groups.yml +++ b/tools/notebooks/topic_groups.yml @@ -4,7 +4,6 @@ groups: - ConfidenceIntervalOverview - ConfigCollExamples - CovariateExamples - - nSTATPaperExamples - TrialConfigExamples smoke: - AnalysisExamples @@ -48,3 +47,15 @@ groups: - ValidationDataSet - nSTATPaperExamples - nSpikeTrainExamples + helpfile_full: + - AnalysisExamples + - DecodingExample + - DecodingExampleWithHist + - ExplicitStimulusWhiskerData + - HippocampalPlaceCellExample + - HybridFilterExample + - PPSimExample + - StimulusDecode2D + - TrialExamples + - ValidationDataSet + - nSTATPaperExamples