diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc0dcfef..9b7a81e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,3 +197,18 @@ jobs: python -m pip install -e .[dev] - name: Run API surface checks run: pytest -q tests/test_api_surface.py + + symbol-surface-audit: + 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] + - name: Verify audited MATLAB-facing runtime surface + run: pytest -q tests/test_matlab_symbol_surface.py tests/test_class_fidelity_audit.py tests/test_parity_report.py diff --git a/nstat/class_fidelity.py b/nstat/class_fidelity.py new file mode 100644 index 00000000..3fd41a34 --- /dev/null +++ b/nstat/class_fidelity.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import importlib +from pathlib import Path +from typing import Any + +import yaml + + +EXPECTED_RUNTIME_MEMBERS: dict[str, tuple[str, ...]] = { + "nstat.Analysis": ( + "GLMFit", + "RunAnalysisForNeuron", + "RunAnalysisForAllNeurons", + "KSPlot", + "computeKSStats", + "computeFitResidual", + "plotFitResidual", + "plotInvGausTrans", + "plotSeqCorr", + "plotCoeffs", + ), + "nstat.CIF": ( + "setSpikeTrain", + "setHistory", + "simulateCIF", + "simulateCIFByThinning", + "simulateCIFByThinningFromLambda", + "evalGradient", + "evalGradientLog", + "evalJacobian", + "evalJacobianLog", + "evalGradientLDGamma", + "evalJacobianLDGamma", + ), + "nstat.DecodingAlgorithms": ( + "PPDecode_predict", + "PPDecode_update", + "PPDecode_updateLinear", + "PPDecodeFilterLinear", + "PPDecodeFilter", + "PP_fixedIntervalSmoother", + "PPHybridFilterLinear", + "PPHybridFilter", + ), +} + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def load_class_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" / "class_fidelity.yml" + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def resolve_public_symbol(dotted_name: str | None) -> Any | None: + if not dotted_name: + return None + parts = [part for part in str(dotted_name).split(".") if part] + if not parts: + return None + obj: Any = importlib.import_module(parts[0]) + for part in parts[1:]: + obj = getattr(obj, part) + return obj + + +def _coerce_verified_flag(value: Any) -> bool: + if isinstance(value, bool): + return value + return str(value).strip().lower() in {"1", "true", "yes"} + + +def row_runtime_symbol_verified(row: dict[str, Any]) -> bool: + public_name = row.get("python_public_name") + symbol = resolve_public_symbol(public_name) + if symbol is None: + return False + required_members = EXPECTED_RUNTIME_MEMBERS.get(str(public_name), ()) + return all(callable(getattr(symbol, member, None)) for member in required_members) + + +def row_audit_symbol_verified(row: dict[str, Any]) -> bool: + return _coerce_verified_flag(row.get("symbol_presence_verified")) + + +def iter_symbol_presence_mismatches(payload: dict[str, Any]) -> list[dict[str, Any]]: + mismatches: list[dict[str, Any]] = [] + for row in payload.get("items", []): + expected = row_runtime_symbol_verified(row) + if row_audit_symbol_verified(row) != expected: + mismatches.append(row) + return mismatches + + +def summarize_symbol_presence(payload: dict[str, Any]) -> dict[str, int]: + counts = {"verified": 0, "unverified": 0, "not_applicable": 0} + for row in payload.get("items", []): + if not row.get("python_public_name") or row.get("status") == "not_applicable": + counts["not_applicable"] += 1 + elif row_runtime_symbol_verified(row): + counts["verified"] += 1 + else: + counts["unverified"] += 1 + return counts + + +__all__ = [ + "EXPECTED_RUNTIME_MEMBERS", + "iter_symbol_presence_mismatches", + "load_class_fidelity_audit", + "resolve_public_symbol", + "row_audit_symbol_verified", + "row_runtime_symbol_verified", + "summarize_symbol_presence", +] diff --git a/nstat/matlab_reference.py b/nstat/matlab_reference.py index dd2864b3..576606b9 100644 --- a/nstat/matlab_reference.py +++ b/nstat/matlab_reference.py @@ -104,8 +104,34 @@ def run_simulated_network_reference(*, matlab_repo: str | Path | None = None, se assignin('base','S2',S{2}); assignin('base','H2',H{2}); assignin('base','E2',E{2}); assignin('base','mu2',mu{2}); options = simget; [tout,~,yout] = sim('SimulatedNetwork2',[stim.minTime stim.maxTime],options,stim.dataToStructure); + [h1Num, ~] = tfdata(H{1}, 'v'); + [h2Num, ~] = tfdata(H{2}, 'v'); + [s1Num, ~] = tfdata(S{1}, 'v'); + [s2Num, ~] = tfdata(S{2}, 'v'); + [e1Num, ~] = tfdata(E{1}, 'v'); + [e2Num, ~] = tfdata(E{2}, 'v'); + stateMat = yout(:,1:2); + probMat = zeros(size(stateMat)); + for n = 1:size(stateMat, 1) + hist1 = 0; hist2 = 0; + for lag = 1:length(h1Num) + if n-lag >= 1 + hist1 = hist1 + h1Num(lag) * stateMat(n-lag,1); + hist2 = hist2 + h2Num(lag) * stateMat(n-lag,2); + end + end + ens1 = 0; ens2 = 0; + if n > 1 + ens1 = e1Num(1) * stateMat(n-1,2); + ens2 = e2Num(1) * stateMat(n-1,1); + end + eta1 = mu{1} + hist1 + s1Num(1) * u(n) + ens1; + eta2 = mu{2} + hist2 + s2Num(1) * u(n) + ens2; + probMat(n,1) = exp(eta1) / (1 + exp(eta1)); + probMat(n,2) = exp(eta2) / (1 + exp(eta2)); + end netSpikeCounts = [sum(yout(:,1)>.5), sum(yout(:,2)>.5)]; - netProbHead = yout(1:5,3:4); + netProbHead = probMat(1:5,:); netStateHead = yout(1:5,1:2); netActual = [0 1; -4 0]; """, @@ -134,11 +160,14 @@ def run_analysis_reference(*, matlab_repo: str | Path | None = None) -> dict[str cfg = TrialConfig({{'Stimulus', 'stim'}}, 10, [], []); cfg.setName('stim'); fit = Analysis.RunAnalysisForNeuron(trial, 1, ConfigColl({cfg})); + summary = FitResSummary({fit}); analysisAIC = fit.AIC(1); analysisBIC = fit.BIC(1); analysisLogLL = fit.logLL(1); analysisCoeffs = fit.getCoeffs(1)'; analysisLambdaHead = fit.lambda.data(1:5, 1)'; + analysisSummaryAIC = summary.AIC(1); + analysisSummaryBIC = summary.BIC(1); """, nargout=0, ) @@ -148,6 +177,8 @@ def run_analysis_reference(*, matlab_repo: str | Path | None = None) -> dict[str "logll": _to_numpy(engine.workspace["analysisLogLL"]).reshape(-1), "coeffs": _to_numpy(engine.workspace["analysisCoeffs"]).reshape(-1), "lambda_head": _to_numpy(engine.workspace["analysisLambdaHead"]).reshape(-1), + "summary_aic": _to_numpy(engine.workspace["analysisSummaryAIC"]).reshape(-1), + "summary_bic": _to_numpy(engine.workspace["analysisSummaryBIC"]).reshape(-1), } diff --git a/nstat/parity_report.py b/nstat/parity_report.py index d349bb05..07e08fe7 100644 --- a/nstat/parity_report.py +++ b/nstat/parity_report.py @@ -5,6 +5,11 @@ import yaml +from nstat.class_fidelity import ( + iter_symbol_presence_mismatches, + load_class_fidelity_audit, + summarize_symbol_presence, +) from nstat.notebook_parity import ( iter_outstanding_notebook_fidelity, load_notebook_parity_notes, @@ -42,12 +47,6 @@ def load_parity_manifest(repo_root: Path | None = None) -> dict[str, Any]: return yaml.safe_load(path.read_text(encoding="utf-8")) -def load_class_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" / "class_fidelity.yml" - return yaml.safe_load(path.read_text(encoding="utf-8")) - - def _summarize_class_fidelity(payload: dict[str, Any]) -> dict[str, int]: counts = {status: 0 for status in payload.get("status_legend", [])} for row in payload.get("items", []): @@ -86,6 +85,8 @@ def render_parity_report(repo_root: Path | None = None) -> str: notebook_fidelity = load_notebook_parity_notes(repo_root) simulink_fidelity = load_simulink_fidelity_audit(repo_root) class_counts = _summarize_class_fidelity(class_fidelity) + symbol_counts = summarize_symbol_presence(class_fidelity) + symbol_mismatches = iter_symbol_presence_mismatches(class_fidelity) notebook_counts = summarize_notebook_fidelity(notebook_fidelity) notebook_partial = iter_outstanding_notebook_fidelity(notebook_fidelity) simulink_counts = summarize_simulink_strategies(simulink_fidelity) @@ -99,7 +100,7 @@ def render_parity_report(repo_root: Path | None = None) -> str: lines = [ "# nSTAT Python Parity Report", "", - "Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/notebooks/parity_notes.yml`.", + "Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebooks/parity_notes.yml`, and live runtime inspection of the audited Python public surface.", "", f"- MATLAB reference: {payload['source_repositories']['matlab']}", f"- Python target: {payload['source_repositories']['python']}", @@ -130,6 +131,19 @@ def render_parity_report(repo_root: Path | None = None) -> str: for status in class_fidelity.get("status_legend", []): lines.append(f"| `{status}` | {class_counts.get(status, 0)} |") + lines.extend( + [ + "", + "## Runtime Symbol Verification", + "", + "| Status | Count |", + "|---|---:|", + f"| `verified` | {symbol_counts['verified']} |", + f"| `unverified` | {symbol_counts['unverified']} |", + f"| `not_applicable` | {symbol_counts['not_applicable']} |", + ] + ) + lines.extend( [ "", @@ -183,6 +197,12 @@ def render_parity_report(repo_root: Path | None = None) -> str: 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 not symbol_mismatches: + lines.append("- Runtime symbol verification: every audited MATLAB-facing Python symbol marked present in `parity/class_fidelity.yml` resolves on the live public surface.") + else: + lines.append( + f"- Runtime symbol verification: {len(symbol_mismatches)} audited MATLAB-facing entries do not currently resolve on the live public surface." + ) if simulink_outstanding: lines.append( f"- Simulink fidelity: {len(simulink_outstanding)} Simulink-backed assets still rely on partial, fallback, or unsupported Python execution paths." @@ -237,6 +257,17 @@ 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(["", "## Runtime Symbol Drift", ""]) + if not symbol_mismatches: + lines.append("No audit/runtime symbol mismatches were detected.") + else: + for row in symbol_mismatches: + label = row.get("matlab_name") or row.get("python_public_name") or row.get("matlab_path") + public_name = row.get("python_public_name") or "None" + lines.append( + f"- `{label}` -> `{public_name}`: `symbol_presence_verified` does not match live runtime resolution." + ) + lines.extend(["", "## Simulink Fidelity Deltas", ""]) if not simulink_outstanding and not simulink_reference_only: lines.append("No partial, reference-only, fallback, or unsupported Simulink execution paths remain in the audit.") diff --git a/nstat/release_check.py b/nstat/release_check.py index e8848f8d..800a11ae 100644 --- a/nstat/release_check.py +++ b/nstat/release_check.py @@ -41,6 +41,8 @@ def build_release_gate_commands( "tests/test_signalobj_fidelity.py", "tests/test_nspiketrain_fidelity.py", "tests/test_workflow_fidelity.py", + "tests/test_class_fidelity_audit.py", + "tests/test_matlab_symbol_surface.py", "tests/test_matlab_reference.py", "tests/test_simulink_fidelity_audit.py", "tests/test_parity_report.py", diff --git a/parity/README.md b/parity/README.md index e0123ae4..e6b6417f 100644 --- a/parity/README.md +++ b/parity/README.md @@ -26,9 +26,15 @@ Run the combined Python plus MATLAB release gate: nstat-release-check --matlab-repo ../nSTAT ``` +Run the MATLAB-side `pyenv` fidelity suite from the sibling MATLAB repo: + +```bash +matlab -batch "cd('../nSTAT'); addpath(fullfile(pwd,'tools','python')); results = runtests('tests/python_port_fidelity'); assertSuccess(results); exit" +``` + 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. +- Class-fidelity auditing is tracked separately from name-mapping parity in `class_fidelity.yml`, and it now records `symbol_presence_verified` so the audit can distinguish prose parity from live runtime symbol resolution. - 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. diff --git a/parity/class_fidelity.yml b/parity/class_fidelity.yml index 37c663eb..5cb67a5d 100644 --- a/parity/class_fidelity.yml +++ b/parity/class_fidelity.yml @@ -35,6 +35,7 @@ items: though warning text and some edge-case errors are still not exact. output_type_parity: MATLAB-facing methods return SignalObj/Covariate instances where expected. + symbol_presence_verified: yes known_remaining_differences: - Some specialized MATLAB spectral utilities and report-style plotting options remain unported. @@ -69,13 +70,15 @@ items: path is matched exactly. output_type_parity: Covariate methods return Covariate or SignalObj as MATLAB expects for the implemented subset. + symbol_presence_verified: yes 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. + - Extend the committed MATLAB-derived fixtures beyond `computeMeanPlusCI` and + explicit confidence-interval payloads to cover CI plotting and serialized + round-trips. plotting_report_parity: Core signal and confidence-interval plotting works; some MATLAB CI styling/report variations remain lighter. - matlab_name: nspikeTrain @@ -103,6 +106,7 @@ items: plotting/statistics edge cases are still not exact. output_type_parity: Signal representation returns SignalObj and rate conversion returns SignalObj as expected. + symbol_presence_verified: yes known_remaining_differences: - Some MATLAB visual styling and distribution-fit detail in the ISI plotting helpers remains lighter than MATLAB. @@ -132,11 +136,13 @@ items: error_warning_parity: Core validation is present, though MATLAB warning text and some edge-case messages still differ. output_type_parity: PSTH returns Covariate. + symbol_presence_verified: yes 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 + - Extend the committed MATLAB-derived fixtures beyond collection naming and + `dataToMatrix` outputs to cover 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 @@ -165,6 +171,7 @@ items: output_type_parity: Matrix-producing methods intentionally return NumPy arrays, while MATLAB-facing object-producing workflows return Trial/CovColl/nstColl-compatible objects where expected. + symbol_presence_verified: yes known_remaining_differences: - Some MATLAB plotting, partition-serialization, and specialized workflow helpers remain unported. @@ -193,6 +200,7 @@ items: 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. + symbol_presence_verified: yes known_remaining_differences: - Some MATLAB normalization and validation branches remain looser in Python. required_remediation: @@ -218,6 +226,7 @@ items: error_warning_parity: Basic validation exists, though some MATLAB collection-coercion edge cases are still looser. output_type_parity: Returns TrialConfig instances. + symbol_presence_verified: yes known_remaining_differences: - Some MATLAB-specific collection manipulation helpers remain unported. required_remediation: @@ -247,6 +256,7 @@ items: 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. + symbol_presence_verified: yes known_remaining_differences: - Advanced MATLAB algorithm-selection, cross-validation, and some report-layout branches are still lighter than MATLAB. @@ -282,6 +292,7 @@ items: and reporting edge cases. output_type_parity: Returns canonical FitResult objects with MATLAB-style aliases and list/array fields. + symbol_presence_verified: yes known_remaining_differences: - Plotting/report methods now execute, but their numerical detail and layout remain lighter than MATLAB. @@ -309,12 +320,13 @@ items: 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. + symbol_presence_verified: yes known_remaining_differences: - Summary plotting now exists, but richer MATLAB report/table exports remain visually lighter than MATLAB. required_remediation: - - Add golden fixtures for multi-neuron summary aggregation and remaining report - outputs. + - Extend the committed golden fixtures beyond single-fit AIC/BIC aggregation to + multi-neuron summary aggregation and remaining report outputs. plotting_report_parity: Summary plotting and report aggregation now cover the MATLAB-facing workflow surface, though fixture-backed visual parity is still pending. - matlab_name: CIF @@ -339,6 +351,7 @@ items: remain thinner. output_type_parity: Returns rate arrays, Covariates, and spike-train collections in the expected workflow positions. + symbol_presence_verified: yes known_remaining_differences: - Simulink-backed recursive-CIF behavior is represented by a native Python implementation, but it is not yet fixture-matched one-for-one against MATLAB/Simulink stochastic @@ -377,12 +390,13 @@ items: exceptions. output_type_parity: MATLAB-facing methods now return tuple outputs and state/covariance tensors instead of only Python-specific dictionaries. + symbol_presence_verified: yes known_remaining_differences: - Target-estimation augmentation, EM routines, and some advanced symbolic-CIF workflows remain thinner than MATLAB. required_remediation: - - Add MATLAB-derived numerical fixtures for DecodingExample, DecodingExampleWithHist, - StimulusDecode2D, and HybridFilterExample. + - Extend the committed MATLAB-derived numerical fixtures beyond `PPDecode_predict` + to DecodingExample, DecodingExampleWithHist, StimulusDecode2D, and HybridFilterExample. - 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 @@ -406,6 +420,7 @@ items: some malformed-input branches remain thinner. output_type_parity: Returns CovariateCollection outputs in the MATLAB-facing workflows that consume History objects. + symbol_presence_verified: yes known_remaining_differences: - Plotting and some specialized history-basis utilities remain unported. required_remediation: @@ -431,6 +446,7 @@ items: error_warning_parity: Core validation now matches MATLAB intent, though plotting-related behaviors remain absent. output_type_parity: Returns canonical Events objects. + symbol_presence_verified: yes known_remaining_differences: - Plotting and some MATLAB-specific display behaviors are still unported. required_remediation: @@ -454,6 +470,7 @@ items: edge cases remain lighter. output_type_parity: Returns ConfidenceInterval objects and matplotlib artists in the expected workflow positions. + symbol_presence_verified: yes known_remaining_differences: - Full MATLAB serialization/display semantics remain lighter than the original toolbox. required_remediation: @@ -481,6 +498,7 @@ items: and malformed-selector branches are still thinner. output_type_parity: Returns Covariate and CovariateCollection-compatible outputs across MATLAB-facing workflows. + symbol_presence_verified: yes known_remaining_differences: - Some structure serialization and rarely used helper methods remain unported. required_remediation: @@ -503,6 +521,7 @@ items: 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. + symbol_presence_verified: yes known_remaining_differences: - Python returns native path types/strings rather than MATLAB cells. required_remediation: @@ -527,6 +546,7 @@ items: notes. output_type_parity: Returns Python dictionaries/status text rather than MATLAB console-only behavior. + symbol_presence_verified: yes known_remaining_differences: - MATLAB path management is intentionally non-applicable in Python. required_remediation: @@ -545,6 +565,7 @@ items: indexing_parity: N/A error_warning_parity: N/A output_type_parity: N/A + symbol_presence_verified: no known_remaining_differences: - Python uses Sphinx docs pages instead of the MATLAB help browser. required_remediation: diff --git a/parity/report.md b/parity/report.md index 30ff20cf..49fe2239 100644 --- a/parity/report.md +++ b/parity/report.md @@ -1,6 +1,6 @@ # nSTAT Python Parity Report -Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/notebooks/parity_notes.yml`. +Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebooks/parity_notes.yml`, and live runtime inspection of the audited Python public surface. - MATLAB reference: https://github.com/cajigaslab/nSTAT - Python target: https://github.com/cajigaslab/nSTAT-python @@ -29,6 +29,14 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/no | `missing` | 0 | | `not_applicable` | 1 | +## Runtime Symbol Verification + +| Status | Count | +|---|---:| +| `verified` | 18 | +| `unverified` | 0 | +| `not_applicable` | 1 | + ## Notebook Fidelity Summary | Status | Count | @@ -56,6 +64,7 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, and `tools/no - 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, wrapper-only, or missing items. +- Runtime symbol verification: every audited MATLAB-facing Python symbol marked present in `parity/class_fidelity.yml` resolves on the live public surface. - Simulink fidelity: native Python coverage exists for the required published workflows, and 10 inventoried MATLAB assets remain reference-only. ## Remaining Mapping Deltas @@ -70,6 +79,10 @@ No partial or missing items remain in the mapping inventory. No partial, wrapper-only, or missing class-fidelity items remain. +## Runtime Symbol Drift + +No audit/runtime symbol mismatches were detected. + ## Simulink Fidelity Deltas - `PointProcessSimulationCont` -> `PointProcessSimulationCont.slx` [reference_only/reference_only]: Keep as reference while the Python port uses the native discrete simulation path. diff --git a/parity/simulink_fidelity.yml b/parity/simulink_fidelity.yml index 40739395..52a5b7c0 100644 --- a/parity/simulink_fidelity.yml +++ b/parity/simulink_fidelity.yml @@ -23,7 +23,7 @@ items: - The native Python path mirrors the Simulink transfer-function semantics for the published help workflows, but not every internal Simulink block configuration has a one-to-one Python analogue. validation_plan: - Compare deterministic lambda traces against MATLAB Engine reference runs when MATLAB is available. - - Keep seeded Python regression tests for FIR filtering, recursive history terms, and PPSimExample outputs in CI. + - Keep committed MATLAB gold fixtures for the leading lambda trace, plus seeded Python regression tests for FIR filtering, recursive history terms, and PPSimExample outputs in CI. - model_name: PointProcessSimulationCont model_path: PointProcessSimulationCont.slx purpose: Continuous-time companion model kept with the MATLAB toolbox for simulation/reference work. @@ -134,7 +134,8 @@ items: - Exact spike trains still differ from Simulink because MATLAB and NumPy do not share the same binomial random stream. - The native port mirrors the published NetworkTutorial parameterization and one-sample-delay semantics, but not every internal Simulink block detail is separately exposed. validation_plan: - - Keep deterministic regression tests for the native simulator parameters, probability traces, and estimated network layout in CI. + - Keep deterministic regression tests for the native simulator parameters, probability traces, binary state traces, and estimated network layout in CI. + - Keep committed MATLAB gold fixtures for `prob_head` and `state_head`, and treat seeded spike-count summaries as tolerance-based because MATLAB and NumPy random streams are not identical. - Run optional MATLAB Engine smoke comparisons for the actual connectivity layout when MATLAB is available. - model_name: SimulatedNetwork2Cache model_path: helpfiles/SimulatedNetwork2.slxc diff --git a/tests/parity/fixtures/matlab_gold/analysis_exactness.mat b/tests/parity/fixtures/matlab_gold/analysis_exactness.mat index 8e1e89f1..effc52b0 100644 Binary files a/tests/parity/fixtures/matlab_gold/analysis_exactness.mat and b/tests/parity/fixtures/matlab_gold/analysis_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/cif_exactness.mat b/tests/parity/fixtures/matlab_gold/cif_exactness.mat index ddd9e0bf..2be3498a 100644 Binary files a/tests/parity/fixtures/matlab_gold/cif_exactness.mat and b/tests/parity/fixtures/matlab_gold/cif_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/covariate_exactness.mat b/tests/parity/fixtures/matlab_gold/covariate_exactness.mat new file mode 100644 index 00000000..45b3a6b6 Binary files /dev/null and b/tests/parity/fixtures/matlab_gold/covariate_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/decoding_predict_exactness.mat b/tests/parity/fixtures/matlab_gold/decoding_predict_exactness.mat new file mode 100644 index 00000000..9b06adf9 Binary files /dev/null and b/tests/parity/fixtures/matlab_gold/decoding_predict_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/nspiketrain_exactness.mat b/tests/parity/fixtures/matlab_gold/nspiketrain_exactness.mat index b43b32f5..8c5ae069 100644 Binary files a/tests/parity/fixtures/matlab_gold/nspiketrain_exactness.mat and b/tests/parity/fixtures/matlab_gold/nspiketrain_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/nstcoll_exactness.mat b/tests/parity/fixtures/matlab_gold/nstcoll_exactness.mat new file mode 100644 index 00000000..d3c89e49 Binary files /dev/null and b/tests/parity/fixtures/matlab_gold/nstcoll_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/point_process_exactness.mat b/tests/parity/fixtures/matlab_gold/point_process_exactness.mat index bccbd6bd..181a2466 100644 Binary files a/tests/parity/fixtures/matlab_gold/point_process_exactness.mat and b/tests/parity/fixtures/matlab_gold/point_process_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/signalobj_exactness.mat b/tests/parity/fixtures/matlab_gold/signalobj_exactness.mat index f3e667f3..b3128bbb 100644 Binary files a/tests/parity/fixtures/matlab_gold/signalobj_exactness.mat and b/tests/parity/fixtures/matlab_gold/signalobj_exactness.mat differ diff --git a/tests/parity/fixtures/matlab_gold/simulated_network_exactness.mat b/tests/parity/fixtures/matlab_gold/simulated_network_exactness.mat new file mode 100644 index 00000000..3cc2ec3e Binary files /dev/null and b/tests/parity/fixtures/matlab_gold/simulated_network_exactness.mat differ diff --git a/tests/test_class_fidelity_audit.py b/tests/test_class_fidelity_audit.py index 264ecb74..99d6c064 100644 --- a/tests/test_class_fidelity_audit.py +++ b/tests/test_class_fidelity_audit.py @@ -4,6 +4,8 @@ import yaml +from nstat.class_fidelity import iter_symbol_presence_mismatches + REPO_ROOT = Path(__file__).resolve().parents[1] AUDIT_PATH = REPO_ROOT / "parity" / "class_fidelity.yml" @@ -73,9 +75,21 @@ def test_class_fidelity_audit_uses_requested_field_names() -> None: "method_parity", "defaults_parity", "indexing_parity", + "symbol_presence_verified", "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')}" + + +def test_class_fidelity_audit_uses_yes_no_symbol_presence_flags() -> None: + payload = _load_audit() + for item in payload["items"]: + assert str(item["symbol_presence_verified"]).strip().lower() in {"true", "false", "yes", "no"} + + +def test_class_fidelity_symbol_presence_matches_runtime_resolution() -> None: + payload = _load_audit() + assert not iter_symbol_presence_mismatches(payload) diff --git a/tests/test_matlab_gold_fixtures.py b/tests/test_matlab_gold_fixtures.py index 8c87b029..33ed0d07 100644 --- a/tests/test_matlab_gold_fixtures.py +++ b/tests/test_matlab_gold_fixtures.py @@ -5,7 +5,22 @@ import numpy as np from scipy.io import loadmat -from nstat import Analysis, CIF, ConfigColl, CovColl, Covariate, SignalObj, Trial, TrialConfig, nspikeTrain, nstColl +from nstat import ( + Analysis, + CIF, + ConfidenceInterval, + ConfigColl, + CovColl, + Covariate, + DecodingAlgorithms, + FitResSummary, + SignalObj, + Trial, + TrialConfig, + nspikeTrain, + nstColl, + simulate_two_neuron_network, +) REPO_ROOT = Path(__file__).resolve().parents[1] @@ -105,6 +120,32 @@ def test_nspiketrain_matches_matlab_gold_fixture() -> None: np.testing.assert_allclose(burst_train.numSpikesPerBurst, _vector(payload, "burst_numSpikesPerBurst"), rtol=1e-8, atol=1e-10) +def test_covariate_and_confidence_interval_match_matlab_gold_fixture() -> None: + payload = _load_fixture("covariate_exactness.mat") + time = _vector(payload, "time") + replicates = np.asarray(payload["replicates"], dtype=float) + + cov = Covariate(time, replicates, "Stimulus", "time", "s", "a.u.", ["r1", "r2", "r3", "r4"]) + mean_cov = cov.computeMeanPlusCI(0.05) + cov_single = Covariate(time, np.mean(replicates, axis=1), "StimulusSingle", "time", "s", "a.u.", ["stim"]) + cov_single.setConfInterval(ConfidenceInterval(time, np.asarray(payload["explicit_ci"], dtype=float), "b")) + + np.testing.assert_allclose(mean_cov.data[:, 0], _vector(payload, "mean_data"), rtol=1e-8, atol=1e-10) + np.testing.assert_allclose(mean_cov.ci[0].bounds, np.asarray(payload["mean_ci"], dtype=float), rtol=1e-8, atol=1e-10) + np.testing.assert_allclose(cov_single.ci[0].bounds, np.asarray(payload["explicit_ci"], dtype=float), rtol=1e-8, atol=1e-10) + + +def test_nstcoll_matches_matlab_gold_fixture() -> None: + payload = _load_fixture("nstcoll_exactness.mat") + n1 = nspikeTrain(_vector(payload, "firstSpikeTimes"), "1", 10.0, 0.0, 0.5, "time", "s", "spikes", "spk", -1) + n2 = nspikeTrain(_vector(payload, "secondSpikeTimes"), "2", 10.0, 0.0, 0.5, "time", "s", "spikes", "spk", -1) + coll = nstColl([n1, n2]) + + np.testing.assert_equal(coll.numSpikeTrains, int(_scalar(payload, "numSpikeTrains"))) + assert coll.getNST(1).name == _string(payload, "firstName") + np.testing.assert_allclose(coll.dataToMatrix([1, 2], 0.1, 0.0, 0.5), np.asarray(payload["dataMatrix"], dtype=float), rtol=1e-8, atol=1e-10) + + def test_cif_eval_surface_matches_matlab_gold_fixture() -> None: payload = _load_fixture("cif_exactness.mat") cif = CIF( @@ -135,13 +176,17 @@ def test_analysis_fit_surface_matches_matlab_gold_fixture() -> None: trial = Trial(nstColl([spike_train]), CovColl([stim])) cfg = TrialConfig([["Stimulus", "stim"]], sample_rate, [], [], name="stim") fit = Analysis.RunAnalysisForNeuron(trial, 1, ConfigColl([cfg])) + summary = FitResSummary([fit]) np.testing.assert_allclose(fit.getCoeffs(1), _vector(payload, "coeffs"), rtol=1e-6, atol=1e-8) np.testing.assert_allclose(fit.lambdaSignal.time, _vector(payload, "lambda_time"), rtol=1e-12, atol=1e-12) np.testing.assert_allclose(fit.lambdaSignal.data[:, 0], _vector(payload, "lambda_data"), rtol=1e-8, atol=1e-10) np.testing.assert_allclose(float(fit.AIC[0]), _scalar(payload, "AIC"), rtol=1e-8, atol=1e-10) np.testing.assert_allclose(float(fit.BIC[0]), _scalar(payload, "BIC"), rtol=1e-8, atol=1e-10) + np.testing.assert_allclose(float(summary.AIC[0]), _scalar(payload, "summaryAIC"), rtol=1e-8, atol=1e-10) + np.testing.assert_allclose(float(summary.BIC[0]), _scalar(payload, "summaryBIC"), rtol=1e-8, atol=1e-10) assert np.isfinite(float(fit.logLL[0])) + assert np.isfinite(float(summary.logLL[0])) assert fit.fitType[0] == _string(payload, "distribution") @@ -165,3 +210,37 @@ def test_point_process_lambda_trace_matches_matlab_gold_fixture() -> None: ) np.testing.assert_allclose(lambda_cov.data[: _vector(payload, 'lambda_head').shape[0], 0], _vector(payload, "lambda_head"), rtol=1e-8, atol=1e-10) + + +def test_decoding_predict_matches_matlab_gold_fixture() -> None: + payload = _load_fixture("decoding_predict_exactness.mat") + x_p, W_p = DecodingAlgorithms.PPDecode_predict( + _vector(payload, "x_u"), + np.asarray(payload["W_u"], dtype=float), + np.asarray(payload["A"], dtype=float), + np.asarray(payload["Q"], dtype=float), + ) + + np.testing.assert_allclose(x_p, _vector(payload, "x_p"), rtol=1e-8, atol=1e-10) + np.testing.assert_allclose(W_p, np.asarray(payload["W_p"], dtype=float), rtol=1e-8, atol=1e-10) + + +def test_simulated_network_matches_matlab_gold_fixture() -> None: + payload = _load_fixture("simulated_network_exactness.mat") + native = simulate_two_neuron_network(seed=4) + + np.testing.assert_allclose(native.actual_network, np.asarray(payload["actual_network"], dtype=float), rtol=1e-12, atol=1e-12) + np.testing.assert_allclose(native.lambda_delta[:5], np.asarray(payload["prob_head"], dtype=float), rtol=1e-8, atol=1e-10) + dt = float(native.time[1] - native.time[0]) + native_state_head = np.column_stack([ + native.spikes.getNST(1).getSigRep(dt, float(native.time[0]), float(native.time[-1])).data[:5, 0], + native.spikes.getNST(2).getSigRep(dt, float(native.time[0]), float(native.time[-1])).data[:5, 0], + ]) + np.testing.assert_allclose(native_state_head, np.asarray(payload["state_head"], dtype=float), rtol=1e-8, atol=1e-10) + native_counts = np.array([ + len(native.spikes.getNST(1).spikeTimes), + len(native.spikes.getNST(2).spikeTimes), + ], dtype=float) + matlab_counts = _vector(payload, "spike_counts") + assert native_counts.shape == matlab_counts.shape + assert np.all(np.abs(native_counts - matlab_counts) <= 64.0) diff --git a/tests/test_matlab_reference.py b/tests/test_matlab_reference.py index 086ad06e..49416780 100644 --- a/tests/test_matlab_reference.py +++ b/tests/test_matlab_reference.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from nstat import Analysis, CIF, ConfigColl, CovColl, Covariate, Trial, TrialConfig, nspikeTrain, nstColl, simulate_two_neuron_network +from nstat import Analysis, CIF, ConfigColl, CovColl, Covariate, FitResSummary, Trial, TrialConfig, nspikeTrain, nstColl, simulate_two_neuron_network from nstat.matlab_reference import ( matlab_engine_available, run_analysis_reference, @@ -73,7 +73,14 @@ def test_native_network_simulation_preserves_matlab_connectivity_layout_when_eng np.testing.assert_allclose(native.actual_network, matlab_ref["actual_network"]) np.testing.assert_allclose(native.lambda_delta[:5], matlab_ref["prob_head"], rtol=1e-6, atol=1e-8) - assert np.all((matlab_ref["state_head"] == 0.0) | (matlab_ref["state_head"] == 1.0)) + dt = float(native.time[1] - native.time[0]) + native_state_head = np.column_stack([ + native.spikes.getNST(1).getSigRep(dt, float(native.time[0]), float(native.time[-1])).data[:5, 0], + native.spikes.getNST(2).getSigRep(dt, float(native.time[0]), float(native.time[-1])).data[:5, 0], + ]) + np.testing.assert_allclose(native_state_head, matlab_ref["state_head"], rtol=1e-6, atol=1e-8) + native_counts = np.array([len(native.spikes.getNST(1).spikeTimes), len(native.spikes.getNST(2).spikeTimes)], dtype=float) + assert np.all(np.abs(native_counts - matlab_ref["spike_counts"]) <= 64.0) @pytest.mark.skipif(not MATLAB_REPO_ROOT.exists(), reason="MATLAB reference repo not available") @@ -87,10 +94,13 @@ def test_native_analysis_fit_matches_matlab_reference_when_engine_is_available() trial = Trial(nstColl([spike_train]), CovColl([stim])) cfg = TrialConfig([["Stimulus", "stim"]], 10.0, [], [], name="stim") fit = Analysis.RunAnalysisForNeuron(trial, 1, ConfigColl([cfg])) + summary = FitResSummary([fit]) matlab_ref = run_analysis_reference(matlab_repo=MATLAB_REPO_ROOT) np.testing.assert_allclose(fit.getCoeffs(1), matlab_ref["coeffs"], rtol=1e-6, atol=1e-8) np.testing.assert_allclose(fit.lambdaSignal.data[:5, 0], matlab_ref["lambda_head"], rtol=1e-6, atol=1e-8) np.testing.assert_allclose(np.asarray(fit.AIC, dtype=float)[:1], matlab_ref["aic"], rtol=1e-6, atol=1e-8) np.testing.assert_allclose(np.asarray(fit.BIC, dtype=float)[:1], matlab_ref["bic"], rtol=1e-6, atol=1e-8) + np.testing.assert_allclose(np.asarray(summary.AIC, dtype=float)[:1], matlab_ref["summary_aic"], rtol=1e-6, atol=1e-8) + np.testing.assert_allclose(np.asarray(summary.BIC, dtype=float)[:1], matlab_ref["summary_bic"], rtol=1e-6, atol=1e-8) assert np.isfinite(np.asarray(fit.logLL, dtype=float)[0]) diff --git a/tests/test_matlab_symbol_surface.py b/tests/test_matlab_symbol_surface.py index 7c70779f..4df82c35 100644 --- a/tests/test_matlab_symbol_surface.py +++ b/tests/test_matlab_symbol_surface.py @@ -2,55 +2,21 @@ import inspect -from nstat import Analysis, CIF, DecodingAlgorithms - - -EXPECTED_SYMBOLS = { - Analysis: { - "RunAnalysisForNeuron", - "RunAnalysisForAllNeurons", - "GLMFit", - "KSPlot", - "plotFitResidual", - "computeFitResidual", - "computeKSStats", - "plotInvGausTrans", - "plotSeqCorr", - "plotCoeffs", - }, - CIF: { - "setSpikeTrain", - "setHistory", - "simulateCIFByThinningFromLambda", - "simulateCIF", - "evalGradient", - "evalGradientLog", - "evalJacobian", - "evalJacobianLog", - "evalGradientLDGamma", - "evalJacobianLDGamma", - }, - DecodingAlgorithms: { - "PPDecode_predict", - "PPDecode_update", - "PPDecode_updateLinear", - "PPDecodeFilterLinear", - "PPDecodeFilter", - "PP_fixedIntervalSmoother", - "PPHybridFilterLinear", - "PPHybridFilter", - }, -} +from nstat.class_fidelity import EXPECTED_RUNTIME_MEMBERS, resolve_public_symbol def test_expected_matlab_symbol_surface_exists_and_is_callable() -> None: - for obj, expected in EXPECTED_SYMBOLS.items(): + for public_name, expected in EXPECTED_RUNTIME_MEMBERS.items(): + obj = resolve_public_symbol(public_name) + assert obj is not None, f"{public_name} does not resolve on the Python public surface" missing = sorted(name for name in expected if not callable(getattr(obj, name, None))) - assert not missing, f"{obj.__name__} is missing MATLAB-facing callables: {missing}" + assert not missing, f"{public_name} is missing MATLAB-facing callables: {missing}" def test_expected_symbol_surface_has_python_runtime_signatures() -> None: - for obj, expected in EXPECTED_SYMBOLS.items(): + for public_name, expected in EXPECTED_RUNTIME_MEMBERS.items(): + obj = resolve_public_symbol(public_name) + assert obj is not None, f"{public_name} does not resolve on the Python public surface" for name in expected: signature = inspect.signature(getattr(obj, name)) assert signature is not None diff --git a/tests/test_parity_report.py b/tests/test_parity_report.py index cbbb1bf8..ca0c9a14 100644 --- a/tests/test_parity_report.py +++ b/tests/test_parity_report.py @@ -20,6 +20,7 @@ def test_parity_report_highlights_current_constraints() -> None: assert "paper examples and docs gallery" in text.lower() assert "all canonical paper examples and committed gallery directories are mapped" in text assert "class fidelity" in text.lower() + assert "Runtime Symbol Verification" in text assert "Notebook Fidelity Summary" in text assert "Simulink Fidelity Summary" in text assert "Remaining Notebook-Fidelity Deltas" in text @@ -29,6 +30,9 @@ def test_parity_report_highlights_current_constraints() -> None: assert "Remaining Class-Fidelity Deltas" in text assert "the class audit reports no partial, wrapper-only, or missing items" in text assert "No partial, wrapper-only, or missing class-fidelity items remain." in text + assert "Runtime symbol verification: every audited MATLAB-facing Python symbol marked present" in text + assert "Runtime Symbol Drift" in text + assert "No audit/runtime symbol mismatches were detected." in text assert "Simulink Fidelity Deltas" in text assert "native Python coverage exists for the required published workflows" in text assert "reference-only" in text diff --git a/tools/parity/matlab/export_matlab_gold_fixtures.m b/tools/parity/matlab/export_matlab_gold_fixtures.m index 0b4d1fa6..bbae80e3 100644 --- a/tools/parity/matlab/export_matlab_gold_fixtures.m +++ b/tools/parity/matlab/export_matlab_gold_fixtures.m @@ -19,10 +19,14 @@ function export_matlab_gold_fixtures(repoRoot, matlabRepoRoot) end export_signalobj_fixture(fixtureRoot); +export_covariate_fixture(fixtureRoot); export_nspiketrain_fixture(fixtureRoot); +export_nstcoll_fixture(fixtureRoot); export_cif_fixture(fixtureRoot); export_analysis_fixture(fixtureRoot); export_point_process_fixture(fixtureRoot); +export_decoding_predict_fixture(fixtureRoot); +export_simulated_network_fixture(fixtureRoot); end function export_signalobj_fixture(fixtureRoot) @@ -99,6 +103,41 @@ function export_nspiketrain_fixture(fixtureRoot) save(fullfile(fixtureRoot, 'nspiketrain_exactness.mat'), '-struct', 'payload'); end +function export_covariate_fixture(fixtureRoot) +t = (0:0.1:0.4)'; +replicates = [0.0 0.1 0.2 0.3; 0.2 0.3 0.4 0.5; 0.4 0.5 0.6 0.7; 0.6 0.7 0.8 0.9; 0.8 0.9 1.0 1.1]; +cov = Covariate(t, replicates, 'Stimulus', 'time', 's', 'a.u.', {'r1','r2','r3','r4'}); +meanCov = cov.computeMeanPlusCI(0.05); +ci = ConfidenceInterval(t, [mean(replicates,2)-0.1, mean(replicates,2)+0.1], 'CI', 'time', 's', 'a.u.'); +covSingle = Covariate(t, mean(replicates,2), 'StimulusSingle', 'time', 's', 'a.u.', {'stim'}); +covSingle.setConfInterval(ci); + +payload = struct(); +payload.time = t; +payload.replicates = replicates; +payload.mean_data = meanCov.data; +payload.mean_ci = meanCov.ci{1}.data; +payload.explicit_ci = covSingle.ci{1}.data; + +save(fullfile(fixtureRoot, 'covariate_exactness.mat'), '-struct', 'payload'); +end + +function export_nstcoll_fixture(fixtureRoot) +n1 = nspikeTrain([0.1 0.3], '1', 10, 0.0, 0.5, 'time', 's', 'spikes', 'spk', -1); +n2 = nspikeTrain([0.2], '2', 10, 0.0, 0.5, 'time', 's', 'spikes', 'spk', -1); +coll = nstColl({n1, n2}); +dataMat = coll.dataToMatrix([1 2], 0.1, 0.0, 0.5); + +payload = struct(); +payload.numSpikeTrains = coll.numSpikeTrains; +payload.firstName = coll.getNST(1).name; +payload.dataMatrix = dataMat; +payload.firstSpikeTimes = coll.getNST(1).spikeTimes; +payload.secondSpikeTimes = coll.getNST(2).spikeTimes; + +save(fullfile(fixtureRoot, 'nstcoll_exactness.mat'), '-struct', 'payload'); +end + function export_cif_fixture(fixtureRoot) cif = CIF([0.1 0.5], {'stim1', 'stim2'}, {'stim1', 'stim2'}, 'binomial'); stimVal = [0.6; -0.2]; @@ -124,6 +163,7 @@ function export_analysis_fixture(fixtureRoot) cfg = TrialConfig({{'Stimulus', 'stim'}}, 10, [], []); cfg.setName('stim'); fit = Analysis.RunAnalysisForNeuron(trial, 1, ConfigColl({cfg})); +summary = FitResSummary({fit}); payload = struct(); payload.time = t; @@ -137,6 +177,9 @@ function export_analysis_fixture(fixtureRoot) payload.BIC = fit.BIC(1); payload.logLL = fit.logLL(1); payload.distribution = fit.fitType{1}; +payload.summaryAIC = summary.AIC(1); +payload.summaryBIC = summary.BIC(1); +payload.summarylogLL = summary.logLL(1); save(fullfile(fixtureRoot, 'analysis_exactness.mat'), '-struct', 'payload'); end @@ -165,3 +208,73 @@ function export_point_process_fixture(fixtureRoot) save(fullfile(fixtureRoot, 'point_process_exactness.mat'), '-struct', 'payload'); end + +function export_decoding_predict_fixture(fixtureRoot) +x_u = [0.1; -0.2]; +W_u = [1.0 0.1; 0.1 2.0]; +A = [1.0 0.2; 0.0 0.9]; +Q = 0.05 * eye(2); +[x_p, W_p] = DecodingAlgorithms.PPDecode_predict(x_u, W_u, A, Q); + +payload = struct(); +payload.x_u = x_u; +payload.W_u = W_u; +payload.A = A; +payload.Q = Q; +payload.x_p = x_p; +payload.W_p = W_p; + +save(fullfile(fixtureRoot, 'decoding_predict_exactness.mat'), '-struct', 'payload'); +end + +function export_simulated_network_fixture(fixtureRoot) +rng(4); +Ts = .001; +t = (0:Ts:50)'; +mu{1} = -3; mu{2} = -3; %#ok +H{1} = tf([-4 -2 -1], [1], Ts, 'Variable', 'z^-1'); %#ok +H{2} = tf([-4 -2 -1], [1], Ts, 'Variable', 'z^-1'); %#ok +S{1} = tf([1], 1, Ts, 'Variable', 'z^-1'); %#ok +S{2} = tf([-1], 1, Ts, 'Variable', 'z^-1'); %#ok +E{1} = tf([1], 1, Ts, 'Variable', 'z^-1'); %#ok +E{2} = tf([-4], 1, Ts, 'Variable', 'z^-1'); %#ok +stim = Covariate(t, sin(2*pi*1*t), 'Stimulus', 'time', 's', 'Voltage', {'sin'}); +assignin('base', 'S1', S{1}); assignin('base', 'H1', H{1}); assignin('base', 'E1', E{1}); assignin('base', 'mu1', mu{1}); +assignin('base', 'S2', S{2}); assignin('base', 'H2', H{2}); assignin('base', 'E2', E{2}); assignin('base', 'mu2', mu{2}); +options = simget; +[~,~,yout] = sim('SimulatedNetwork2', [stim.minTime stim.maxTime], options, stim.dataToStructure); +[h1Num, ~] = tfdata(H{1}, 'v'); +[h2Num, ~] = tfdata(H{2}, 'v'); +[s1Num, ~] = tfdata(S{1}, 'v'); +[s2Num, ~] = tfdata(S{2}, 'v'); +[e1Num, ~] = tfdata(E{1}, 'v'); +[e2Num, ~] = tfdata(E{2}, 'v'); +stateMat = yout(:,1:2); +probMat = zeros(size(stateMat)); +for n = 1:size(stateMat, 1) + hist1 = 0; hist2 = 0; + for lag = 1:length(h1Num) + if n-lag >= 1 + hist1 = hist1 + h1Num(lag) * stateMat(n-lag,1); + hist2 = hist2 + h2Num(lag) * stateMat(n-lag,2); + end + end + ens1 = 0; ens2 = 0; + if n > 1 + ens1 = e1Num(1) * stateMat(n-1,2); + ens2 = e2Num(1) * stateMat(n-1,1); + end + eta1 = mu{1} + hist1 + s1Num(1) * stim.data(n) + ens1; + eta2 = mu{2} + hist2 + s2Num(1) * stim.data(n) + ens2; + probMat(n,1) = exp(eta1) / (1 + exp(eta1)); + probMat(n,2) = exp(eta2) / (1 + exp(eta2)); +end + +payload = struct(); +payload.actual_network = [0 1; -4 0]; +payload.prob_head = probMat(1:5,:); +payload.state_head = yout(1:5,1:2); +payload.spike_counts = [sum(yout(:,1) > .5), sum(yout(:,2) > .5)]; + +save(fullfile(fixtureRoot, 'simulated_network_exactness.mat'), '-struct', 'payload'); +end