Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Auto detect text files and perform LF normalization
* text=auto

# Large parity artifacts: keep diffs out of review and treat as generated.
tests/_r_cache.json -diff linguist-generated
tools/NNS_13.0.tar.gz binary
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Build artifacts
/build/
/dist/
*.so

# Virtual environments
.venv/

# R cache lock
tests/_r_cache.lock
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# NNS Python

Python port of the R NNS 12.1 beta package.
Python port of the R NNS 13.0 package.

- Distribution package: `NNS`
- Import package: `nns` (`import nns`)
Expand Down
8 changes: 4 additions & 4 deletions docs/api_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This page summarizes the public NNS Python API surface, known gaps, guarded paths,
and design boundaries.

NNS Python is an alpha, parity-focused Python port of installed R NNS 12.1 beta,
NNS Python is an alpha, parity-focused Python port of installed R NNS 13.0,
implemented natively in Python on top of NumPy and SciPy. It does not
wrap R, call the R package at runtime, or depend on compiled R/C++ shims. The
goal is public input/output compatibility where R behavior is stable,
Expand Down Expand Up @@ -54,7 +54,7 @@ invariant, and property coverage.
| Boost: `nns_boost` | partial | medium | Deterministic and stochastic structures are implemented; one high-feature threshold path remains guarded to match installed-R failure behavior. |
| Seasonality: `nns_seas` | implemented | high | Non-plotting installed-R path is implemented and cached defensively. |
| ARMA and VAR: `nns_arma`, `nns_arma_optim`, `nns_var` | partial | medium | Numeric forecasting and supported VAR dimension-reduction paths are implemented on focused fixtures. Explicit numeric multi-lag ARMA uses actual-lag weighting instead of installed R's position-based weighting quirk. VAR's multivariate stack stage matches R's effective time-series holdout sizing; the remaining macro-like VAR strict xfail is inherited from ARMA optimizer period selection. Stochastic interval streams are structural/statistical parity only. |
| Nowcast panel: `nns_nowcast_panel` | implemented | medium | Python-native deterministic monthly panel helper backed by `nns_var`. R NNS 12.1 beta removed `NNS.nowcast`, so this is no longer an R-export parity target. |
| Nowcast panel: `nns_nowcast_panel` | implemented | medium | Python-native deterministic monthly panel helper backed by `nns_var`. R NNS 13.0 does not export `NNS.nowcast`, so this is no longer an R-export parity target. |
| Providers: `CsvNowcastProvider` | implemented | medium | Produces explicit local/offline payloads for `nns_nowcast_panel`. |
| Bootstrap/Monte Carlo: `nns_meboot`, `nns_mc` | implemented | medium | Deterministic diagnostics are parity-tested; exact stochastic replicate parity with R is not expected. |
| Stochastic dominance/superiority: `fsd`, `ssd`, `tsd`, `.uni` wrappers, `nns_ss`, `nns_sd_cluster`, `sd_efficient_set` | implemented | medium | Public structures and deterministic paths are covered. SD uses exact pure-NumPy prefix-pair kernels plus a degree-1 discrete order-statistic matrix path; R's C++ core remains faster on full finance fixtures. Stochastic intervals use NNS Python RNG. |
Expand All @@ -75,7 +75,7 @@ invariant, and property coverage.
## Intentional Design Boundaries

- No hidden network fetching happens by default.
- NNS Python does not export `nns_nowcast`; R NNS 12.1 beta removed `NNS.nowcast`.
- NNS Python does not export `nns_nowcast`; R NNS 13.0 does not export `NNS.nowcast`.
- Nowcast providers are payload builders for `nns_nowcast_panel`, not implicit
public forecast wrappers.
- `CsvNowcastProvider` is local/offline.
Expand Down Expand Up @@ -132,7 +132,7 @@ examples include:
classification vignette, the documented ARMA numeric multi-lag weighting
divergence, and VAR's ARMA-derived univariate/ensemble outputs. The Iris
classification xfail mixes two different issues: NNS Python stack predicts the
correct held-out class where installed R NNS 12.1 rounds the same borderline
correct held-out class where installed R NNS 13.0 rounds the same borderline
estimate down, while boost remains a true output disparity whose installed-R
and NNS Python balanced predictions both miss the held-out class.

Expand Down
4 changes: 2 additions & 2 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ uv run python scripts/update_benchmarks_doc.py docs/benchmark_reports/benchmark_

## Results

R baselines use installed R NNS 12.1.
R baselines use installed R NNS 13.0.

`Python speed vs R` is computed as `R baseline / Python mean`. Values above `1.00x` mean Python is faster; values below `1.00x` mean Python is slower.

Expand Down Expand Up @@ -123,7 +123,7 @@ baseline so Python/R comparisons remain visible when R has not been rerun.
Run only the realistic Python benchmarks with:

```bash
NNS_OFFLINE=1 uv run pytest -q -n0 -m benchmark --benchmark-enable \
PYNNS_OFFLINE=1 uv run pytest -q -n0 -m benchmark --benchmark-enable \
--benchmark-json=docs/benchmark_reports/realistic_sd_python_latest.json \
tests/benchmarks/test_stochastic_dominance_realistic.py \
tests/benchmarks/test_finance_sd_rolling.py \
Expand Down
27 changes: 13 additions & 14 deletions docs/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

## Build

NNS Python is packaged as the `NNS` distribution and imported with `import nns`.
It includes the `nns._nnscore` native extension backed by the vendored C++ core in
`extern/NNS-core`, while preserving Python fallbacks for the public APIs that route
through the native backend. CI parity is cache-backed and does not require
`Rscript`; `Rscript` is only needed for local cache regeneration.
NNS Python is currently a pure-Python/NumPy/SciPy port. The earlier native extension
scaffolding was removed after the core port demonstrated pure NumPy/SciPy parity
and competitive performance. Reintroduce native code only as a deliberate future
change backed by benchmarks.

## Degree-Zero Boundary

Expand Down Expand Up @@ -134,7 +133,7 @@ supported.

`nns_part` maps to R's `NNS.part` but returns plain NumPy arrays instead of
`data.table` objects: `"dt"` and `"regression.points"` are dictionaries of
arrays. Installed R 12.1 only distinguishes `type = NULL` from any non-null
arrays. Installed R 13.0 only distinguishes `type = NULL` from any non-null
`type`: `None` uses XY quadrant splits, while every non-`None` value uses
X-only splits. This differs from documentation that implies separate `"X"`,
`"Y"`, and `"XONLY"` modes. NNS Python matches the installed binary.
Expand Down Expand Up @@ -267,16 +266,16 @@ counterintuitive. R's `CV.size = NULL` samples a random value between 0.2 and
1/3; NNS Python uses a deterministic default of `0.25`. Pass `cv_size` explicitly for
exact R parity.

The installed-R 12.1 Iris classification vignette with `folds=1` is a documented
The installed-R 13.0 Iris classification vignette with `folds=1` is a documented
stack disparity rather than a NNS Python correctness target. On the `141:150` holdout,
the true labels are all class code `3`. Installed R 12.1 returns stack class code
the true labels are all class code `3`. Installed R 13.0 returns stack class code
`2` for every row because its learned class-rounding threshold is about `0.60`;
NNS Python returns class code `3` for every row because its learned threshold is about
`0.29`. Both implementations have the same high-level shape in that case
(`reg = 2`, `dim.red = 3`, raw combined stack near `2.5`), but the final
threshold rounding differs. Since R default `folds=5` also returns class code
`3`, NNS Python keeps the behavior that matches the practical classification result
instead of forcing installed-R-12.1 `folds=1` parity.
instead of forcing installed-R-13.0 `folds=1` parity.

Factor predictor expansion is supported for `nns_stack(method=1)` and
`nns_stack(method=2)` with explicit `factor_levels=` metadata. NNS Python expands
Expand Down Expand Up @@ -329,11 +328,11 @@ enabled. The public `n.best` value is structural-only because R's final internal
`NNS.stack` call samples its own `CV.size = NULL` split, while NNS Python keeps the
deterministic stack default.

The installed-R 12.1 Iris boost vignette remains a true parity gap, but not a
The installed-R 13.0 Iris boost vignette remains a true parity gap, but not a
quality target for exact output matching. On the same all-class-`3` holdout,
installed R 12.1 balanced boost returns class code `1` for every row, while NNS Python
installed R 13.0 balanced boost returns class code `1` for every row, while NNS Python
balanced boost returns class code `2` for every row; both are wrong for that
example. Installed R 12.1 also does not accept the `folds` argument shown in the
example. Installed R 13.0 also does not accept the `folds` argument shown in the
rendered upstream overview for `NNS.boost`, so this example is tracked as
R-version/upstream-example drift plus a boost parity gap rather than evidence
that NNS Python should copy the installed-R balanced output.
Expand Down Expand Up @@ -411,7 +410,7 @@ to numeric series, delegates numeric forecasting to `nns_var`, and returns VAR
fields plus `dates` and `metadata` dictionaries. Date labels are metadata rather
than array indices. Without dates, forecast rows are labeled `t+1`, `t+2`, ...
With dates, inputs are normalized to `YYYY-MM`, must be sorted and unique, and
forecast labels advance monthly. R NNS 12.1 beta removed `NNS.nowcast`, so NNS Python
forecast labels advance monthly. R NNS 13.0 does not export `NNS.nowcast`, so NNS Python
does not export a public `nns_nowcast` wrapper. `CsvNowcastProvider` remains an
explicit payload builder whose `fetch(series, start_date)` method returns
`{"series": ..., "dates": ..., "metadata": ...}` for callers to pass to
Expand Down Expand Up @@ -462,7 +461,7 @@ helpers. NNS Python accepts `rpm` as a finite 2D numeric array with R's `y.hat`
column in the final position. `nns_distance` applies R's per-target min-max
rescaling before computing weighted nearest-neighbor predictions. `nns_distance_bulk`
matches R's compiled bulk helper, including its raw-feature distance convention.
For `nns_distance` with `k > 1`, NNS Python matches the installed R 12.1 binary:
For `nns_distance` with `k > 1`, NNS Python matches the installed R 13.0 binary:
the exponential rank-weight family uses the R C API's `Rf_dexp` scale argument
as `1 / k`. This differs from the nearby source-code comment that describes it
as a rate.
Expand Down
6 changes: 2 additions & 4 deletions docs/examples/notebooks/03_forecasting_nowcast_workflow.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,7 @@
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Local nowcast panel\nR NNS 12.1 removed `NNS.nowcast`; NNS Python keeps the local panel workflow.\n"
]
"source": "## Local nowcast panel\nR NNS 13.0 does not export `NNS.nowcast`; NNS Python keeps the local panel workflow.\n"
},
{
"cell_type": "code",
Expand Down Expand Up @@ -272,4 +270,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
4 changes: 2 additions & 2 deletions docs/original_tests_adoption.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@
- `PM.matrix` matrices remain NumPy-first arrays without R-style dimnames;
labels are available only via the optional `names` echo described above.

## Scope notes
## Out of scope

The official package identity is now the `NNS` distribution with `import nns` and native extension `nns._nnscore`. Full R package parity is not claimed; parity remains bounded by the committed fixtures and cache entries, and plot artifacts remain intentionally out of scope.
The NNS-python migration remains out of scope. The `nns` package name is unchanged.
100 changes: 65 additions & 35 deletions docs/parity_plan.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,83 @@
# Parity Plan

This branch completes the pre-migration parity suite for `NNS-python-core-backed` while keeping the `NNS-python` migration out of scope.
## Target

## Closed gap workstream (branch `close-all-parity-gaps`)
Retarget Python parity to R NNS 13.0. R NNS 13.0 is the tensorized architecture target, and R NNS 12.1 cache data is superseded. NNS-core is v13.0.0 and remains the native C++ foundation.

The gaps previously tracked in `docs/parity_results.md` are now closed or
formally resolved:
## Plan

1. **`nns_boost` cache-parity failure** — triaged as seed-sensitivity on the
CV-split path for an unseeded call. The boosted result is empirically
seed-invariant and matches the committed R cache to ~3.5e-15; the parity test
now pins a seed and a `test_nns_boost_ivs_test_none_is_seed_invariant`
regression guard was added.
2. **`NNS.copula` discrete mode** — implemented (`continuous=False`) and adopted.
3. **`NNS.copula` multivariate / three-column** — implemented (matrix input) and
adopted for both continuous and discrete.
4. **`PM.matrix` data-frame naming** — optional NumPy-first `names` echo added
with a parity test; numeric behavior unchanged.
5. **Plot / graphics policy** — formalized in `docs/plot_parity_policy.md`.
6. **Skips** — the only remaining skips are intentional live-R-only practical
examples (not cache-backed parity gaps).
1. Install R and R dependencies.
2. Install R NNS 13.0 from the vendored package source under `tools/` (never from CRAN).
3. Confirm `packageVersion("NNS") == "13.0"`.
4. Validate the R NNS 13.0 smoke values for partial moments, copula, ARMA, regression points, PM matrix naming, and seeded stack behavior.
5. Regenerate `tests/_r_cache.json` with R NNS 13.0 metadata and values.
6. Run cache-only parity, capture the full failure inventory, and fix Python behavior to R NNS 13.0 without loosening tolerances.
7. Keep full parity claims bounded by tests and cache.
8. Keep plot artifact policy unchanged.

## Scope
## Installing R NNS 13.0 from local source

- Preserve public-behavior parity tests against R NNS 12.1 through `tests/parity/`.
- Keep R calls isolated in the test harness and cache tooling.
- Allow CI to run parity checks without `Rscript` by using committed cache fixtures with `NNS_R_CACHE_ONLY=1`.
- Preserve native-vs-Python fallback coverage for partial moments and related helpers.
- Preserve the merged PR #6 fix that blocks non-finite partial-moment inputs from native dispatch.
The vendored R package source is committed in this repository, so NNS is installed
from local source, not CRAN:

## Cache workflow
- Extracted package directory: `tools/NNS` (`tools/NNS/DESCRIPTION` reports `Version: 13.0`).
- Vendored tarball: `tools/NNS_13.0.tar.gz`.

- `tests/_r_cache.json` is the committed R-compatible cache used by CI.
- `NNS_R_CACHE_ONLY=1` forces cache-only parity and must be used in CI.
- To refresh cache entries on a workstation with R and NNS installed, run:
Install with the helper script (prefers `tools/NNS`, falls back to the tarball, and
verifies the loaded version):

```bash
python scripts/regenerate_r_cache.py
python scripts/install_local_r_nns.py
```

Pass pytest selectors after `--` to refresh a narrower subset, for example:
Or run the exact command sequence directly:

```bash
python scripts/regenerate_r_cache.py -- tests/parity/test_core.py
R CMD INSTALL tools/NNS
Rscript -e "suppressPackageStartupMessages(library(NNS)); cat(as.character(packageVersion('NNS')))"
# expected output: 13.0
```

## Guardrails
Do not run `install.packages("NNS")`; the parity target is the local `tools/NNS`
source, not the CRAN release.

- Do not require `Rscript` in CI.
- Do not reintroduce stale native expectations for partial moments.
- Do not import `nns.pm_matrix` through the package-level public function when module access is required; use `importlib.import_module("nns.pm_matrix")`.
- Do not route `NaN` or infinite partial-moment inputs through native `lpm`, `upm`, `lpm_ratio`, or `upm_ratio` dispatch.
## Regenerating the parity cache

After confirming `packageVersion("NNS") == "13.0"`, regenerate the committed cache
with cache-only/offline toggles unset:

```bash
unset PYNNS_R_CACHE_ONLY PYNNS_OFFLINE CI
python scripts/regenerate_r_cache.py -- -n 0 tests/parity
```

If full regeneration is slow or unstable, regenerate deterministic chunks one file
at a time, for example `python scripts/regenerate_r_cache.py -- -n 0 tests/parity/test_core.py`,
then continue through the remaining parity files. The committed result must remain a
single valid `tests/_r_cache.json` with `nns_version == "13.0"`, `schema_version == 1`,
and non-empty `entries`. `scripts/regenerate_r_cache.py` enforces those guardrails after
the pytest run.

Validate the regenerated cache offline:

```bash
PYNNS_R_CACHE_ONLY=1 python -m pytest -q -n 0 tests/parity
```

A `RuntimeError: R cache miss ...` means the cache is incomplete (regenerate the
missing live R entries); an `AssertionError`/numeric mismatch means Python behavior
differs from R NNS 13.0 and the Python implementation must be fixed without loosening
tolerances.

## Current retarget focus

The first fixed root cause is the `NNS.reg(..., multivariate.call = TRUE)` regression-point construction used by nonlinear ARMA. Python now preserves R NNS 13.0's duplicate central-point contribution during endpoint consolidation.

## Environment note

The committed `tests/_r_cache.json` carries `nns_version == "13.0"` and `schema_version == 1`
with non-empty `entries`, and the full cache-only parity suite passes against it. Where an R
toolchain is unavailable (for example, sandboxed CI or proxy-restricted runners that cannot
install R), the cache cannot be regenerated live; rerun the local-source install and
`scripts/regenerate_r_cache.py` on a host with R when refreshing the cache. Always install NNS
from `tools/NNS` (or `tools/NNS_13.0.tar.gz`), never from CRAN.
Loading
Loading