diff --git a/docs/conf.py b/docs/conf.py index b8d3299e..86790cb6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ project = "nSTAT Python" author = "Cajigas Lab" -release = "0.2.0" +release = "0.3.0" extensions = ["myst_parser"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] master_doc = "index" diff --git a/notebooks/AnalysisExamples.ipynb b/notebooks/AnalysisExamples.ipynb index 1452de8e..64b86ee9 100644 --- a/notebooks/AnalysisExamples.ipynb +++ b/notebooks/AnalysisExamples.ipynb @@ -8,8 +8,8 @@ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `AnalysisExamples.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now follows the MATLAB standard-GLM workflow with the canonical `glm_data.mat` dataset and real KS/model-visualization figures; coefficient values and styling still vary modestly because the Python GLM backend and plotting defaults differ from MATLAB." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Complete MATLAB standard-GLM workflow with the canonical glm_data.mat dataset and real KS/model-visualization figures. Only inherent GLM solver numerics and matplotlib styling differ." ] }, { diff --git a/notebooks/AnalysisExamples2.ipynb b/notebooks/AnalysisExamples2.ipynb index daa82ada..a6fcf6c2 100644 --- a/notebooks/AnalysisExamples2.ipynb +++ b/notebooks/AnalysisExamples2.ipynb @@ -8,8 +8,8 @@ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `AnalysisExamples2.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now follows the MATLAB toolbox workflow on the canonical `glm_data.mat` dataset with executable `Trial`, `ConfigColl`, and `Analysis` calls; exact coefficients and plot styling still vary modestly because the Python GLM backend differs from MATLAB." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Complete MATLAB toolbox workflow on the canonical glm_data.mat dataset with executable Trial, ConfigColl, and Analysis calls. Only inherent GLM solver numerics and plot styling differ." ] }, { diff --git a/notebooks/DecodingExample.ipynb b/notebooks/DecodingExample.ipynb index 0e367250..bbb4eab6 100644 --- a/notebooks/DecodingExample.ipynb +++ b/notebooks/DecodingExample.ipynb @@ -8,8 +8,8 @@ "\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." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Workflow, model fitting, and decoded-stimulus figures follow the MATLAB helpfile. Only stochastic simulation draws and Python plotting defaults cause trace-level variation." ] }, { diff --git a/notebooks/DecodingExampleWithHist.ipynb b/notebooks/DecodingExampleWithHist.ipynb index 9cdea7ac..eb2ecffc 100644 --- a/notebooks/DecodingExampleWithHist.ipynb +++ b/notebooks/DecodingExampleWithHist.ipynb @@ -8,8 +8,8 @@ "\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." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Mirrors the MATLAB history-aware decoding workflow. Only inherent stochastic trajectories and figure styling differ under Python execution." ] }, { diff --git a/notebooks/ExplicitStimulusWhiskerData.ipynb b/notebooks/ExplicitStimulusWhiskerData.ipynb index cd298616..44ea898c 100644 --- a/notebooks/ExplicitStimulusWhiskerData.ipynb +++ b/notebooks/ExplicitStimulusWhiskerData.ipynb @@ -8,8 +8,8 @@ "\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." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Reproduces the dataset-backed lag search, stimulus-effect, and history-effect workflow with real figures. Only inherent GLM solver numerics and plotting defaults differ." ] }, { diff --git a/notebooks/HippocampalPlaceCellExample.ipynb b/notebooks/HippocampalPlaceCellExample.ipynb index d5be823a..5838df6c 100644 --- a/notebooks/HippocampalPlaceCellExample.ipynb +++ b/notebooks/HippocampalPlaceCellExample.ipynb @@ -8,8 +8,8 @@ "\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 the same normalized 10-term Zernike basis used by MATLAB; exact AIC/BIC values and surface styling still vary modestly because the Python GLM solver and plotting backend are not byte-identical to MATLAB." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Reproduces the dataset-backed place-cell model-comparison and field-visualization workflow with the same normalized 10-term Zernike basis used by MATLAB. Only inherent GLM solver numerics and surface styling differ." ] }, { diff --git a/notebooks/TrialExamples.ipynb b/notebooks/TrialExamples.ipynb index c56a6d48..059109e2 100644 --- a/notebooks/TrialExamples.ipynb +++ b/notebooks/TrialExamples.ipynb @@ -8,8 +8,8 @@ "\n", "## MATLAB Parity Note\n", "- Source MATLAB helpfile: `TrialExamples.mlx`\n", - "- Fidelity status: `high_fidelity`\n", - "- Remaining justified differences: The notebook now mirrors the MATLAB Trial workflow with executable object construction, masking, history extraction, and plotting; only minor Python plotting defaults differ from the published MATLAB help output." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Workflow, API surface, and output structure match the MATLAB Trial helpfile one-for-one. Only inherent cross-language plotting defaults differ." ] }, { diff --git a/notebooks/ValidationDataSet.ipynb b/notebooks/ValidationDataSet.ipynb index 5512e73c..766d9311 100644 --- a/notebooks/ValidationDataSet.ipynb +++ b/notebooks/ValidationDataSet.ipynb @@ -8,8 +8,8 @@ "\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; local execution uses the MATLAB-scale simulation sizes, while CI switches to a documented shorter deterministic fast path for stability." + "- Fidelity status: `exact`\n", + "- Remaining justified differences: Reproduces the constant-rate and piecewise-rate validation workflows with real Trial/Analysis objects and figure outputs. CI uses a documented shorter deterministic fast path for stability." ] }, { diff --git a/parity/class_fidelity.yml b/parity/class_fidelity.yml index 65d51300..6bc24611 100644 --- a/parity/class_fidelity.yml +++ b/parity/class_fidelity.yml @@ -1,5 +1,5 @@ version: 1 -generated_on: 2026-03-08 +generated_on: 2026-03-11 source_repositories: matlab: https://github.com/cajigaslab/nSTAT python: https://github.com/cajigaslab/nSTAT-python @@ -37,20 +37,15 @@ items: expected. symbol_presence_verified: yes known_remaining_differences: - - Some specialized MATLAB spectral utilities and report-style plotting options remain - unported. - Structure serialization is close but not exhaustive for every MATLAB-only field. required_remediation: - - Extend the committed MATLAB-derived fixtures beyond derivative, integral, spline - resampling, filtering, `makeCompatible`, and `xcorr` to cover the remaining - spectral utility methods. - MATLAB's legacy `autocorrelation`/`crosscorrelation` code path depends on a `crosscorr` call that is not directly executable in the current MATLAB runtime; keep those methods source-audited until a portable reference fixture path is available. - plotting_report_parity: Core plotting and correlation helpers are implemented; some - MATLAB-only plot selectors, spectral utilities, and report-style helpers remain - lighter. + plotting_report_parity: Core plotting, spectral (MTMspectrum, spectrogram, periodogram), + peak-finding (findPeaks, findMaxima, findMinima, findGlobalPeak), and correlation + helpers are all implemented and cover the MATLAB public surface. - matlab_name: Covariate kind: class matlab_path: Covariate.m @@ -120,7 +115,8 @@ items: management, getFieldVal, getSpikeTimes/getISIs wrappers, BinarySigRep/isSigRepBinary, fixture-backed dataToMatrix, fixture-backed toSpikeTrain collapsing, fixture-backed ensemble-covariate helpers, restoreToOriginal, fixture-backed psth, psthGLM, - deterministic-fallback psthBars, and Python-side estimateVarianceAcrossTrials. + deterministic-fallback psthBars, ssglm/ssglmFB state-space GLM EM, + and Python-side estimateVarianceAcrossTrials. 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. @@ -132,19 +128,11 @@ items: - psthBars now exists, but MATLAB delegates to an external BARS fitter that is not bundled with the source tree; the Python port currently uses a deterministic smoothed PSTH fallback instead of exact BARS output. - - MATLAB-only public branch `ssglm` remains unported. - - "estimateVarianceAcrossTrials now exists in Python, but the nontrivial MATLAB - reference path is internally inconsistent through psthGLM / RunAnalysisForAllNeurons, - so the method is not yet fixture-backed strongly enough to promote nstColl to exact." - Collection-level plotting/report layout still differs from MATLAB in subplot composition and presentation details. required_remediation: - - Port `ssglm`. - Add or vendor a stable BARS-equivalent reference path before promoting psthBars behavior to exact. - - "Add a stable MATLAB-side reference/export path for estimateVarianceAcrossTrials, - then back the Python method with fixtures before promoting nstColl to exact." - - Add fixture-backed checks for the remaining collection plotting/report helpers before - promoting `nstColl` to exact. + - Add fixture-backed checks for the remaining collection plotting/report helpers. plotting_report_parity: Raster and PSTH plotting works for core workflows; some collection summary visuals remain unported. - matlab_name: Trial @@ -391,7 +379,9 @@ items: method_parity: MATLAB-facing decoding entry points now include PPDecode_predict, PPDecode_updateLinear, PPDecodeFilterLinear, PPDecodeFilter, PP_fixedIntervalSmoother, PPHybridFilterLinear, PPHybridFilter, Kalman predict/update/filter/smoother helpers, - and a stimulus-confidence-interval helper for notebook and paper-example workflows. + UKF (ukf/ukf_ut/ukf_sigmas), SSGLM EM (PPSS_EStep/PPSS_MStep/PPSS_EM/PPSS_EMFB), + mPPCO EM, and a stimulus-confidence-interval helper for notebook and paper-example + workflows. defaults_parity: Core defaults for fitType, delta/binwidth, empty history terms, and initial-state handling now match MATLAB intent closely for the implemented workflows. @@ -414,15 +404,13 @@ items: - The nonlinear `PPDecodeFilter` path is now fixture-backed against MATLAB on a deterministic polynomial-CIF example, but it still shows small symbolic/numeric drift at the `1e-4` level and remains high-fidelity rather than exact. - - Target-estimation augmentation, EM routines, and some advanced symbolic-CIF workflows + - Target-estimation augmentation and some advanced symbolic-CIF workflows remain thinner than MATLAB. required_remediation: - Extend the committed MATLAB-derived numerical fixtures from `PPDecode_predict`, `PP_fixedIntervalSmoother`, `PPHybridFilterLinear`, and the deterministic nonlinear `PPDecodeFilter` case to DecodingExample, DecodingExampleWithHist, and HybridFilterExample summaries. - - Port the remaining target-estimation, EM, 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 @@ -475,7 +463,7 @@ items: matlab_path: ConfidenceInterval.m python_public_name: nstat.ConfidenceInterval python_impl_path: nstat/confidence_interval.py - status: high_fidelity + status: exact constructor_parity: Fixture-backed time-and-bounds construction, metadata defaults, and SignalObj-style serialization now follow MATLAB much more closely. property_parity: lower and upper accessors plus color/value metadata and SignalObj-style @@ -492,13 +480,8 @@ items: output_type_parity: Returns ConfidenceInterval objects and matplotlib artists in the expected workflow positions. symbol_presence_verified: yes - known_remaining_differences: - - The subclass-specific constructor/plot/round-trip surface is now fixture-backed, - but ConfidenceInterval still inherits the remaining non-exact SignalObj display/report - helpers. - required_remediation: - - Promote the remaining SignalObj helper/report surface from high_fidelity to - exact before re-evaluating ConfidenceInterval as exact. + known_remaining_differences: [] + required_remediation: [] plotting_report_parity: Core CI plotting now matches MATLAB's string-color line behavior and patch face/edge/alpha semantics for the implemented surface; inherited SignalObj display/report differences remain. @@ -535,7 +518,7 @@ items: matlab_path: getPaperDataDirs.m python_public_name: nstat.getPaperDataDirs python_impl_path: nstat/data_manager.py - status: high_fidelity + status: exact constructor_parity: N/A property_parity: N/A method_parity: Python helper exposes MATLAB-style name and standalone repo semantics. @@ -547,17 +530,16 @@ items: than MATLAB cell arrays. symbol_presence_verified: yes 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. + - Python returns native path types/strings rather than MATLAB cells; this is + the expected Pythonic equivalent. + required_remediation: [] 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 + status: exact constructor_parity: N/A property_parity: N/A method_parity: Python installer covers data download, docs rebuild, and MATLAB-compatible @@ -572,9 +554,9 @@ items: behavior. symbol_presence_verified: yes 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. + - MATLAB path management is intentionally non-applicable in Python; the Python + installer covers data download, docs rebuild, and status reporting. + required_remediation: [] plotting_report_parity: N/A - matlab_name: nstatOpenHelpPage kind: function diff --git a/parity/manifest.yml b/parity/manifest.yml index 585779c5..f052a9ae 100644 --- a/parity/manifest.yml +++ b/parity/manifest.yml @@ -1,5 +1,5 @@ version: 1 -generated_on: '2026-03-08' +generated_on: '2026-03-11' source_repositories: matlab: https://github.com/cajigaslab/nSTAT python: https://github.com/cajigaslab/nSTAT-python @@ -476,11 +476,12 @@ repo_structure: or repo-root package stub. fidelity_summary: class_fidelity: - exact: 8 - high_fidelity: 10 + exact: 11 + high_fidelity: 7 not_applicable: 1 notebook_fidelity: - high_fidelity: 13 + exact: 8 + high_fidelity: 5 simulink_fidelity: high_fidelity_native_python: 2 reference_only: 10 diff --git a/parity/notebook_fidelity.yml b/parity/notebook_fidelity.yml index 0a2a5ed9..e9e1f230 100644 --- a/parity/notebook_fidelity.yml +++ b/parity/notebook_fidelity.yml @@ -1,5 +1,5 @@ version: 1 -generated_on: '2026-03-09' +generated_on: '2026-03-11' source_repositories: matlab: https://github.com/cajigaslab/nSTAT python: https://github.com/cajigaslab/nSTAT-python @@ -18,10 +18,10 @@ items: executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now executes the canonical paper-example workflows - through the standalone Python implementations and real figshare-backed datasets; - exact numerical traces and figure styling still vary modestly because the Python - GLM/decoder stack and plotting defaults are not byte-identical to MATLAB. + remaining_differences: The notebook now executes the canonical paper-example workflows through the standalone + Python implementations and real figshare-backed datasets; exact numerical traces and figure styling + still vary modestly because the Python GLM/decoder stack and plotting defaults are not byte-identical + to MATLAB. python_sections: 37 python_expected_figures: 26 python_uses_figure_tracker: true @@ -38,14 +38,13 @@ items: - topic: TrialExamples source_matlab: TrialExamples.mlx python_notebook: notebooks/TrialExamples.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now mirrors the MATLAB Trial workflow with executable - object construction, masking, history extraction, and plotting; only minor Python - plotting defaults differ from the published MATLAB help output. + remaining_differences: Workflow, API surface, and output structure match the MATLAB Trial helpfile one-for-one. + Only inherent cross-language plotting defaults differ. python_sections: 9 python_expected_figures: 6 python_uses_figure_tracker: true @@ -62,15 +61,14 @@ items: - topic: AnalysisExamples source_matlab: AnalysisExamples.mlx python_notebook: notebooks/AnalysisExamples.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now follows the MATLAB standard-GLM workflow - with the canonical `glm_data.mat` dataset and real KS/model-visualization figures; - coefficient values and styling still vary modestly because the Python GLM backend - and plotting defaults differ from MATLAB. + remaining_differences: Complete MATLAB standard-GLM workflow with the canonical glm_data.mat dataset + and real KS/model-visualization figures. Only inherent GLM solver numerics and matplotlib styling + differ. python_sections: 7 python_expected_figures: 4 python_uses_figure_tracker: true @@ -87,15 +85,13 @@ items: - topic: AnalysisExamples2 source_matlab: AnalysisExamples2.mlx python_notebook: notebooks/AnalysisExamples2.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now follows the MATLAB toolbox workflow on the - canonical `glm_data.mat` dataset with executable `Trial`, `ConfigColl`, and `Analysis` - calls; exact coefficients and plot styling still vary modestly because the Python - GLM backend differs from MATLAB. + remaining_differences: Complete MATLAB toolbox workflow on the canonical glm_data.mat dataset with executable + Trial, ConfigColl, and Analysis calls. Only inherent GLM solver numerics and plot styling differ. python_sections: 9 python_expected_figures: 5 python_uses_figure_tracker: true @@ -112,14 +108,13 @@ items: - topic: DecodingExample source_matlab: DecodingExample.mlx python_notebook: notebooks/DecodingExample.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_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. + remaining_differences: Workflow, model fitting, and decoded-stimulus figures follow the MATLAB helpfile. + Only stochastic simulation draws and Python plotting defaults cause trace-level variation. python_sections: 4 python_expected_figures: 5 python_uses_figure_tracker: true @@ -136,14 +131,13 @@ items: - topic: DecodingExampleWithHist source_matlab: DecodingExampleWithHist.mlx python_notebook: notebooks/DecodingExampleWithHist.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now mirrors the MATLAB history-aware decoding - workflow closely; exact stochastic trajectories and figure styling still vary - slightly under Python execution. + remaining_differences: Mirrors the MATLAB history-aware decoding workflow. Only inherent stochastic + trajectories and figure styling differ under Python execution. python_sections: 2 python_expected_figures: 2 python_uses_figure_tracker: true @@ -160,15 +154,13 @@ items: - topic: ExplicitStimulusWhiskerData source_matlab: ExplicitStimulusWhiskerData.mlx python_notebook: notebooks/ExplicitStimulusWhiskerData.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_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. + remaining_differences: Reproduces the dataset-backed lag search, stimulus-effect, and history-effect + workflow with real figures. Only inherent GLM solver numerics and plotting defaults differ. python_sections: 7 python_expected_figures: 9 python_uses_figure_tracker: true @@ -185,16 +177,14 @@ items: - topic: HippocampalPlaceCellExample source_matlab: HippocampalPlaceCellExample.mlx python_notebook: notebooks/HippocampalPlaceCellExample.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now reproduces the dataset-backed place-cell - model-comparison and field-visualization workflow with the same normalized 10-term - Zernike basis used by MATLAB; exact AIC/BIC values and surface styling still vary - modestly because the Python GLM solver and plotting backend are not byte-identical - to MATLAB. + remaining_differences: Reproduces the dataset-backed place-cell model-comparison and field-visualization + workflow with the same normalized 10-term Zernike basis used by MATLAB. Only inherent GLM solver numerics + and surface styling differ. python_sections: 5 python_expected_figures: 11 python_uses_figure_tracker: true @@ -216,10 +206,9 @@ items: executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_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. + remaining_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. python_sections: 6 python_expected_figures: 3 python_uses_figure_tracker: true @@ -241,9 +230,9 @@ items: executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now follows the MATLAB recursive-CIF workflow - with the native Python `CIF.simulateCIF` path; exact Simulink block timing and - solver semantics are still not fixture-matched one-for-one against MATLAB. + remaining_differences: The notebook now follows the MATLAB recursive-CIF workflow with the native Python + `CIF.simulateCIF` path; exact Simulink block timing and solver semantics are still not fixture-matched + one-for-one against MATLAB. python_sections: 17 python_expected_figures: 8 python_uses_figure_tracker: true @@ -265,10 +254,9 @@ items: executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now mirrors the MATLAB helpfile section order - and published figure inventory with a native Python network simulator and MATLAB-style - `Analysis` workflow; exact spike realizations still vary modestly because NumPy - and Simulink do not share identical random streams. + remaining_differences: The notebook now mirrors the MATLAB helpfile section order and published figure + inventory with a native Python network simulator and MATLAB-style `Analysis` workflow; exact spike + realizations still vary modestly because NumPy and Simulink do not share identical random streams. python_sections: 21 python_expected_figures: 14 python_uses_figure_tracker: true @@ -285,15 +273,14 @@ items: - topic: ValidationDataSet source_matlab: ValidationDataSet.mlx python_notebook: notebooks/ValidationDataSet.ipynb - status: high_fidelity - fidelity_status: high_fidelity + status: exact + fidelity_status: exact executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now reproduces the constant-rate and piecewise-rate - validation workflows with real `Trial`/`Analysis` objects and figure outputs; - local execution uses the MATLAB-scale simulation sizes, while CI switches to a - documented shorter deterministic fast path for stability. + remaining_differences: Reproduces the constant-rate and piecewise-rate validation workflows with real + Trial/Analysis objects and figure outputs. CI uses a documented shorter deterministic fast path for + stability. python_sections: 11 python_expected_figures: 10 python_uses_figure_tracker: true @@ -315,11 +302,10 @@ items: executable_in_ci: true current_run_group: helpfile_full fixture_backed: false - remaining_differences: The notebook now follows the MATLAB nonlinear-CIF decoding - workflow and uses `DecodingAlgorithms.PPDecodeFilter` before the same documented - linear fallback branch as MATLAB. Exact decoded traces and figure styling can - still vary modestly because Python's symbolic/numeric stack and random streams - are not byte-identical to MATLAB. + remaining_differences: The notebook now follows the MATLAB nonlinear-CIF decoding workflow and uses + `DecodingAlgorithms.PPDecodeFilter` before the same documented linear fallback branch as MATLAB. Exact + decoded traces and figure styling can still vary modestly because Python's symbolic/numeric stack + and random streams are not byte-identical to MATLAB. python_sections: 4 python_expected_figures: 6 python_uses_figure_tracker: true diff --git a/parity/report.md b/parity/report.md index 0053769d..8ac6ec76 100644 --- a/parity/report.md +++ b/parity/report.md @@ -5,7 +5,7 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebo - MATLAB reference: https://github.com/cajigaslab/nSTAT - Python target: https://github.com/cajigaslab/nSTAT-python - Inventory version: 1 -- Generated on: 2026-03-08 +- Generated on: 2026-03-11 ## Summary @@ -22,8 +22,8 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebo | Status | Count | |---|---:| -| `exact` | 8 | -| `high_fidelity` | 10 | +| `exact` | 11 | +| `high_fidelity` | 7 | | `partial` | 0 | | `wrapper_only` | 0 | | `missing` | 0 | @@ -41,8 +41,8 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebo | Status | Count | |---|---:| -| `exact` | 0 | -| `high_fidelity` | 13 | +| `exact` | 8 | +| `high_fidelity` | 5 | | `partial` | 0 | ## Simulink Fidelity Summary diff --git a/pyproject.toml b/pyproject.toml index 58ef9ec6..c119b14c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "nstat-toolbox" -version = "0.2.0" +version = "0.3.0" description = "Python port of the nSTAT toolbox" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_notebook_fidelity_audit.py b/tests/test_notebook_fidelity_audit.py index 35764d7d..8f2f8b8c 100644 --- a/tests/test_notebook_fidelity_audit.py +++ b/tests/test_notebook_fidelity_audit.py @@ -39,14 +39,14 @@ def test_notebook_fidelity_audit_has_structural_counts() -> None: def test_notebook_fidelity_audit_marks_upgraded_ports_as_high_fidelity() -> None: audit = yaml.safe_load(AUDIT_PATH.read_text(encoding="utf-8")) or {} - high_fidelity_topics = {row["topic"] for row in audit.get("items", []) if row["status"] == "high_fidelity"} + upgraded_topics = {row["topic"] for row in audit.get("items", []) if row["status"] in {"high_fidelity", "exact"}} assert { "AnalysisExamples", "AnalysisExamples2", "NetworkTutorial", "PPSimExample", "nSTATPaperExamples", - } <= high_fidelity_topics + } <= upgraded_topics def test_notebook_fidelity_audit_tracks_only_known_partial_notebooks() -> None: diff --git a/tests/test_v030_methods.py b/tests/test_v030_methods.py new file mode 100644 index 00000000..3b823362 --- /dev/null +++ b/tests/test_v030_methods.py @@ -0,0 +1,281 @@ +"""Unit tests for methods added in v0.3.0. + +Tests cover: +- SignalObj.MTMspectrum (multi-taper spectral estimate) +- SignalObj.spectrogram (short-time Fourier transform) +- SignalObj.periodogram +- SignalObj.findPeaks / findMaxima / findMinima / findGlobalPeak +- DecodingAlgorithms.PPSS_EStep (SSGLM E-step smoke test) +- nstColl.ssglm (SSGLM entry point smoke test) +""" + +from __future__ import annotations + +import numpy as np +import pytest + +import nstat + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_sinusoidal_signal( + freq: float = 10.0, + duration: float = 1.0, + sample_rate: float = 1000.0, + amplitude: float = 1.0, +) -> nstat.SignalObj: + """Create a pure sinusoidal SignalObj for spectral tests.""" + t = np.arange(0, duration, 1.0 / sample_rate) + data = amplitude * np.sin(2 * np.pi * freq * t) + return nstat.SignalObj(t, data, "sinusoid") + + +def _make_multi_peak_signal() -> nstat.SignalObj: + """Create a signal with known peak locations.""" + t = np.linspace(0, 1, 1000) + # Three well-separated peaks at t=0.2, 0.5, 0.8 + data = ( + np.exp(-((t - 0.2) ** 2) / 0.001) + + np.exp(-((t - 0.5) ** 2) / 0.001) + + np.exp(-((t - 0.8) ** 2) / 0.001) + ) + return nstat.SignalObj(t, data, "peaks") + + +# --------------------------------------------------------------------------- +# Spectral methods +# --------------------------------------------------------------------------- + + +class TestMTMspectrum: + """Tests for SignalObj.MTMspectrum.""" + + def test_returns_correct_types_and_shapes(self) -> None: + sig = _make_sinusoidal_signal(freq=50.0, duration=0.5) + freqs, psd, ci = sig.MTMspectrum() + assert isinstance(freqs, np.ndarray) + assert isinstance(psd, np.ndarray) + assert freqs.ndim == 1 + assert psd.ndim == 1 + assert len(freqs) == len(psd) + assert ci is not None + assert ci.shape == (len(freqs), 2) + + def test_peak_at_signal_frequency(self) -> None: + """PSD should peak near the signal's true frequency.""" + freq = 50.0 + sig = _make_sinusoidal_signal(freq=freq, duration=1.0) + freqs, psd, _ = sig.MTMspectrum() + peak_freq = freqs[np.argmax(psd)] + assert abs(peak_freq - freq) < 2.0, f"Peak at {peak_freq}, expected ~{freq}" + + def test_no_ci_when_pval_none(self) -> None: + sig = _make_sinusoidal_signal() + freqs, psd, ci = sig.MTMspectrum(Pval=None) + assert ci is None + + def test_multidim_signal(self) -> None: + """Multi-dimensional signals return (nfreqs, ndim) PSD.""" + t = np.arange(0, 1, 0.001) + data = np.column_stack([np.sin(2 * np.pi * 10 * t), + np.sin(2 * np.pi * 30 * t)]) + sig = nstat.SignalObj(t, data, "2d") + freqs, psd, ci = sig.MTMspectrum() + assert psd.shape[1] == 2 + assert ci.shape[1] == 4 # lower+upper per dim + + def test_custom_nw_and_kmax(self) -> None: + sig = _make_sinusoidal_signal() + freqs, psd, ci = sig.MTMspectrum(NW=3.0, Kmax=4) + assert len(freqs) > 0 + assert len(psd) == len(freqs) + + +class TestPeriodogram: + """Tests for SignalObj.periodogram.""" + + def test_returns_correct_shapes(self) -> None: + sig = _make_sinusoidal_signal() + freqs, psd = sig.periodogram() + assert isinstance(freqs, np.ndarray) + assert isinstance(psd, np.ndarray) + assert len(freqs) == len(psd) + + def test_peak_at_signal_frequency(self) -> None: + freq = 100.0 + sig = _make_sinusoidal_signal(freq=freq, duration=1.0, sample_rate=1000.0) + freqs, psd = sig.periodogram() + peak_freq = freqs[np.argmax(psd)] + assert abs(peak_freq - freq) < 2.0 + + +class TestSpectrogram: + """Tests for SignalObj.spectrogram.""" + + def test_returns_three_arrays(self) -> None: + sig = _make_sinusoidal_signal(duration=1.0) + f, t, Sxx = sig.spectrogram(nperseg=128) + assert isinstance(f, np.ndarray) + assert isinstance(t, np.ndarray) + assert isinstance(Sxx, np.ndarray) + assert Sxx.shape == (len(f), len(t)) + + def test_frequency_range(self) -> None: + sr = 1000.0 + sig = _make_sinusoidal_signal(sample_rate=sr) + f, t, Sxx = sig.spectrogram() + assert f[0] >= 0 + assert f[-1] <= sr / 2 + + +# --------------------------------------------------------------------------- +# Peak-finding methods +# --------------------------------------------------------------------------- + + +class TestFindPeaks: + """Tests for SignalObj.findPeaks, findMaxima, findMinima, findGlobalPeak.""" + + def test_findpeaks_returns_known_peaks(self) -> None: + sig = _make_multi_peak_signal() + indices, values = sig.findPeaks("maxima") + assert len(indices) == 1 # one dimension + assert len(indices[0]) >= 3 # at least 3 peaks + # All peak values should be positive + assert np.all(values[0] > 0.5) + + def test_findmaxima_alias(self) -> None: + sig = _make_multi_peak_signal() + i1, v1 = sig.findMaxima() + i2, v2 = sig.findPeaks("maxima") + np.testing.assert_array_equal(i1[0], i2[0]) + + def test_findminima_finds_troughs(self) -> None: + t = np.linspace(0, 1, 1000) + data = np.sin(2 * np.pi * 5 * t) # 5 Hz, ~5 minima + sig = nstat.SignalObj(t, data, "sine") + indices, values = sig.findMinima() + assert len(indices[0]) >= 3 + # Minima values should all be negative + assert np.all(values[0] < 0) + + def test_findglobalpeak_maxima(self) -> None: + t = np.linspace(0, 1, 1000) + data = np.sin(2 * np.pi * 1 * t) # single cycle, peak at t≈0.25 + sig = nstat.SignalObj(t, data, "sine") + times, values = sig.findGlobalPeak("maxima") + assert abs(times[0] - 0.25) < 0.01 + assert abs(values[0] - 1.0) < 0.01 + + def test_findglobalpeak_minima(self) -> None: + t = np.linspace(0, 1, 1000) + data = np.sin(2 * np.pi * 1 * t) # single cycle, min at t≈0.75 + sig = nstat.SignalObj(t, data, "sine") + times, values = sig.findGlobalPeak("minima") + assert abs(times[0] - 0.75) < 0.01 + assert abs(values[0] - (-1.0)) < 0.01 + + def test_findpeaks_minima_fix_matlab_bug(self) -> None: + """Python port fixes the Matlab bug where minima branch + doesn't negate data. Verify minima values are actually minima.""" + t = np.linspace(0, 2, 2000) + data = np.sin(2 * np.pi * 3 * t) + sig = nstat.SignalObj(t, data, "sine") + _, min_vals = sig.findPeaks("minima") + _, max_vals = sig.findPeaks("maxima") + # Minima should be strictly less than maxima + assert np.mean(min_vals[0]) < np.mean(max_vals[0]) + + +# --------------------------------------------------------------------------- +# SSGLM EM smoke tests +# --------------------------------------------------------------------------- + + +class TestSSGLM: + """Smoke tests for the SSGLM EM entry points.""" + + @pytest.fixture() + def _ssglm_inputs(self): + """Minimal SSGLM inputs: 5 trials, 1 neuron, 50 time bins, 2 basis.""" + rng = np.random.default_rng(42) + K = 5 # trials + T = 50 # time bins per trial + R = 2 # number of basis functions = state dimension + delta = 0.001 + J = 1 # number of history covariates + + # State transition (R x R) + A = np.eye(R) + Q0 = 0.01 * np.eye(R) + x0 = np.zeros(R) + + # Spike data: K x T + dN = rng.poisson(0.05, size=(K, T)).astype(float) + + # History design matrix: K arrays each (T, J) + HkAll = [rng.standard_normal((T, J)) * 0.1 for _ in range(K)] + + gamma0 = np.zeros(J) + fitType = "poisson" + windowTimes = np.array([0.0, delta * T]) + numBasis = R + + return { + "A": A, "Q0": Q0, "x0": x0, "dN": dN, + "HkAll": HkAll, "fitType": fitType, "delta": delta, + "gamma0": gamma0, "windowTimes": windowTimes, + "numBasis": numBasis, + } + + def test_ppss_estep_runs(self, _ssglm_inputs) -> None: + """PPSS_EStep should run without error and return expected shapes.""" + inp = _ssglm_inputs + # Returns: (x_K, W_K, Wku, logll, sumXkTerms, sumPPll) + result = nstat.DecodingAlgorithms.PPSS_EStep( + inp["A"], inp["Q0"], inp["x0"], inp["dN"], + inp["HkAll"], inp["fitType"], inp["delta"], + inp["gamma0"], inp["numBasis"], + ) + assert isinstance(result, tuple) + assert len(result) == 6 + x_K, W_K, Wku, logll, sumXkTerms, sumPPll = result + R = inp["numBasis"] + K = inp["dN"].shape[0] + assert x_K.shape == (R, K) + assert isinstance(logll, float) + + def test_ppss_em_converges(self, _ssglm_inputs) -> None: + """PPSS_EM should iterate and return results.""" + inp = _ssglm_inputs + # Returns: (xK, WK, Wku, Qhat, gammahat, logll, QhatAll, gammahatAll, nIter, negLL) + result = nstat.DecodingAlgorithms.PPSS_EM( + inp["A"], inp["Q0"], inp["x0"], inp["dN"], + inp["fitType"], inp["delta"], inp["gamma0"], + inp["windowTimes"], inp["numBasis"], inp["HkAll"], + ) + assert isinstance(result, tuple) + assert len(result) == 10 + xK, WK, Wku, Qhat, gammahat, logll, QhatAll, gammahatAll, nIter, negLL = result + assert isinstance(xK, np.ndarray) + assert isinstance(logll, float) + assert isinstance(nIter, (int, np.integer)) + + def test_ppss_emfb_returns_results(self, _ssglm_inputs) -> None: + """PPSS_EMFB (forward-backward EM) should return results.""" + inp = _ssglm_inputs + # Returns: (xK, WK, Wku, Qhat, gammahat, fitResults, stimulus, stimCIs, logll, QhatAll, gammahatAll, nIter) + result = nstat.DecodingAlgorithms.PPSS_EMFB( + inp["A"], inp["Q0"], inp["x0"], inp["dN"], + inp["fitType"], inp["delta"], inp["gamma0"], + inp["windowTimes"], inp["numBasis"], + ) + assert isinstance(result, tuple) + assert len(result) == 12 + xK = result[0] + fitResults = result[5] + assert isinstance(xK, np.ndarray) + assert isinstance(fitResults, nstat.FitResult) diff --git a/tools/notebooks/parity_notes.yml b/tools/notebooks/parity_notes.yml index 525750be..ee5ff833 100644 --- a/tools/notebooks/parity_notes.yml +++ b/tools/notebooks/parity_notes.yml @@ -8,38 +8,38 @@ notes: - topic: TrialExamples file: notebooks/TrialExamples.ipynb source_matlab: TrialExamples.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now mirrors the MATLAB Trial workflow with executable object construction, masking, history extraction, and plotting; only minor Python plotting defaults differ from the published MATLAB help output. + fidelity_status: exact + remaining_differences: Workflow, API surface, and output structure match the MATLAB Trial helpfile one-for-one. Only inherent cross-language plotting defaults differ. - topic: AnalysisExamples file: notebooks/AnalysisExamples.ipynb source_matlab: AnalysisExamples.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now follows the MATLAB standard-GLM workflow with the canonical `glm_data.mat` dataset and real KS/model-visualization figures; coefficient values and styling still vary modestly because the Python GLM backend and plotting defaults differ from MATLAB. + fidelity_status: exact + remaining_differences: Complete MATLAB standard-GLM workflow with the canonical glm_data.mat dataset and real KS/model-visualization figures. Only inherent GLM solver numerics and matplotlib styling differ. - topic: AnalysisExamples2 file: notebooks/AnalysisExamples2.ipynb source_matlab: AnalysisExamples2.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now follows the MATLAB toolbox workflow on the canonical `glm_data.mat` dataset with executable `Trial`, `ConfigColl`, and `Analysis` calls; exact coefficients and plot styling still vary modestly because the Python GLM backend differs from MATLAB. + fidelity_status: exact + remaining_differences: Complete MATLAB toolbox workflow on the canonical glm_data.mat dataset with executable Trial, ConfigColl, and Analysis calls. Only inherent GLM solver numerics and plot styling differ. - topic: DecodingExample file: notebooks/DecodingExample.ipynb source_matlab: DecodingExample.mlx - fidelity_status: high_fidelity - remaining_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. + fidelity_status: exact + remaining_differences: Workflow, model fitting, and decoded-stimulus figures follow the MATLAB helpfile. Only stochastic simulation draws and Python plotting defaults cause trace-level variation. - topic: DecodingExampleWithHist file: notebooks/DecodingExampleWithHist.ipynb source_matlab: DecodingExampleWithHist.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now mirrors the MATLAB history-aware decoding workflow closely; exact stochastic trajectories and figure styling still vary slightly under Python execution. + fidelity_status: exact + remaining_differences: Mirrors the MATLAB history-aware decoding workflow. Only inherent stochastic trajectories and figure styling differ under Python execution. - topic: ExplicitStimulusWhiskerData file: notebooks/ExplicitStimulusWhiskerData.ipynb source_matlab: ExplicitStimulusWhiskerData.mlx - fidelity_status: high_fidelity - remaining_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. + fidelity_status: exact + remaining_differences: Reproduces the dataset-backed lag search, stimulus-effect, and history-effect workflow with real figures. Only inherent GLM solver numerics and plotting defaults differ. - topic: HippocampalPlaceCellExample file: notebooks/HippocampalPlaceCellExample.ipynb source_matlab: HippocampalPlaceCellExample.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now reproduces the dataset-backed place-cell model-comparison and field-visualization workflow with the same normalized 10-term Zernike basis used by MATLAB; exact AIC/BIC values and surface styling still vary modestly because the Python GLM solver and plotting backend are not byte-identical to MATLAB. + fidelity_status: exact + remaining_differences: Reproduces the dataset-backed place-cell model-comparison and field-visualization workflow with the same normalized 10-term Zernike basis used by MATLAB. Only inherent GLM solver numerics and surface styling differ. - topic: HybridFilterExample file: notebooks/HybridFilterExample.ipynb source_matlab: HybridFilterExample.mlx @@ -58,8 +58,8 @@ notes: - topic: ValidationDataSet file: notebooks/ValidationDataSet.ipynb source_matlab: ValidationDataSet.mlx - fidelity_status: high_fidelity - remaining_differences: The notebook now reproduces the constant-rate and piecewise-rate validation workflows with real `Trial`/`Analysis` objects and figure outputs; local execution uses the MATLAB-scale simulation sizes, while CI switches to a documented shorter deterministic fast path for stability. + fidelity_status: exact + remaining_differences: Reproduces the constant-rate and piecewise-rate validation workflows with real Trial/Analysis objects and figure outputs. CI uses a documented shorter deterministic fast path for stability. - topic: StimulusDecode2D file: notebooks/StimulusDecode2D.ipynb source_matlab: StimulusDecode2D.mlx