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
26 changes: 22 additions & 4 deletions src/nns/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
from __future__ import annotations

import warnings

import numpy as np
from numpy.typing import NDArray

from nns._native import nnscore
from nns._native import native_fn


def _warn_unsupported(**was_set: bool) -> None:
"""Warn about R-compatibility parameters that have no effect in NNS Python.

Each keyword names a parameter; pass True when the caller deviated from the
default and would therefore expect the parameter to do something.
"""
ignored = [name for name, flag in was_set.items() if flag]
if ignored:
warnings.warn(
f"{', '.join(ignored)}: accepted for R NNS API compatibility "
"but not implemented in NNS Python; ignored.",
UserWarning,
stacklevel=3,
)


def _fast_lm(x: NDArray[np.float64], y: NDArray[np.float64]) -> tuple[float, float]:
Expand All @@ -17,9 +35,9 @@ def _fast_lm(x: NDArray[np.float64], y: NDArray[np.float64]) -> tuple[float, flo
if x_values.size == 0:
raise ValueError("x and y must be non-empty.")

native = nnscore()
if native is not None and hasattr(native, "fast_lm"):
result = native.fast_lm(np.ascontiguousarray(x_values), np.ascontiguousarray(y_values))
native_fast_lm = native_fn("fast_lm")
if native_fast_lm is not None:
result = native_fast_lm(np.ascontiguousarray(x_values), np.ascontiguousarray(y_values))
coef = result["coef"]
return float(coef[0]), float(coef[1])

Expand Down
7 changes: 7 additions & 0 deletions src/nns/_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@
def nnscore() -> Any | None:
"""Return the optional private NNS-core extension module when available."""
return cast(Any | None, _nnscore)


def native_fn(name: str) -> Any | None:
"""Return a callable from the NNS-core extension, or None when unavailable."""
if _nnscore is None:
return None
return getattr(_nnscore, name, None)
7 changes: 4 additions & 3 deletions src/nns/arma.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np
from numpy.typing import NDArray

from nns._helpers import _fast_lm
from nns._helpers import _fast_lm, _warn_unsupported
from nns.co_moments import co_lpm, co_upm
from nns.dependence import _gravity
from nns.mc import nns_mc
Expand All @@ -32,7 +32,8 @@ def nns_arma_optim(
plot: bool = False,
) -> dict[str, Any]:
"""Optimize seasonal factors for :func:`nns_arma` like R's ``NNS.ARMA.optim``."""
del ncores, print_trace
_warn_unsupported(ncores=ncores is not None)
del print_trace # R console trace flag; NNS Python emits no console output.

values = _as_variable(variable)
original_values = values.copy()
Expand Down Expand Up @@ -365,7 +366,7 @@ def nns_arma(
random_seed: int | None = None,
) -> NDArray[np.float64] | dict[str, NDArray[np.float64]]:
"""Autoregressive NNS forecast matching R's installed NNS.ARMA behavior."""
del seasonal_plot
del seasonal_plot # R plotting side effect; NNS Python returns data instead.

horizon = int(h)
if horizon < 1:
Expand Down
2 changes: 1 addition & 1 deletion src/nns/boost.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def nns_boost(
factor_levels: Sequence[object] | Sequence[Sequence[object] | None] | None = None,
) -> BoostResult:
"""Deterministic NNS.boost port using real NNS.reg and NNS.stack internals."""
del status
del status # R console progress flag; NNS Python emits no console output.
type_value = _normalize_type(type)
if balance:
type_value = "class"
Expand Down
14 changes: 7 additions & 7 deletions src/nns/central_tendencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import numpy as np
from numpy.typing import NDArray

from nns._native import nnscore
from nns._native import native_fn
from nns.dependence import _quartiles_like_r_code, _simple_bin_counts


Expand Down Expand Up @@ -60,10 +60,10 @@ def nns_mode(
if n == 0:
return np.array([np.nan], dtype=np.float64) if multi else float("nan")

native = nnscore()
if native is not None and hasattr(native, "mode"):
native_mode = native_fn("mode")
if native_mode is not None:
native_result = np.asarray(
native.mode(np.ascontiguousarray(finite), discrete, multi), dtype=np.float64
native_mode(np.ascontiguousarray(finite), discrete, multi), dtype=np.float64
)
if multi:
return native_result
Expand All @@ -87,9 +87,9 @@ def nns_gravity(x: NDArray[np.float64], discrete: bool = False) -> float:
if bool(np.all(finite == finite[0])):
return float(finite[0])

native = nnscore()
if native is not None and hasattr(native, "gravity"):
return float(native.gravity(np.ascontiguousarray(finite), discrete))
native_gravity = native_fn("gravity")
if native_gravity is not None:
return float(native_gravity(np.ascontiguousarray(finite), discrete))

value_range = float(abs(finite[-1] - finite[0]))
if value_range == 0.0:
Expand Down
55 changes: 22 additions & 33 deletions src/nns/co_moments.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from typing import Any

import numpy as np
from numpy.typing import NDArray

from nns._native import nnscore
from nns._native import native_fn
from nns.core import _as_degree, _as_targets


Expand Down Expand Up @@ -69,33 +71,19 @@ def _co_moment(
degree_x = _as_degree(degree_x)
degree_y = _as_degree(degree_y)

native = nnscore()
if (
native is not None
and x_targets.size > 0
and y_targets.size > 0
and _native_function_available(native, x_side, y_side)
):
x_contig = np.ascontiguousarray(x_values)
y_contig = np.ascontiguousarray(y_values)
x_targets_contig = np.ascontiguousarray(x_targets)
y_targets_contig = np.ascontiguousarray(y_targets)
if x_side is _lower and y_side is _lower:
native_result = native.co_lpm_v(
degree_x, degree_y, x_contig, y_contig, x_targets_contig, y_targets_contig
)
elif x_side is _upper and y_side is _upper:
native_result = native.co_upm_v(
degree_x, degree_y, x_contig, y_contig, x_targets_contig, y_targets_contig
)
elif x_side is _upper and y_side is _lower:
native_result = native.d_lpm_v(
degree_y, degree_x, x_contig, y_contig, x_targets_contig, y_targets_contig
)
else:
native_result = native.d_upm_v(
degree_x, degree_y, x_contig, y_contig, x_targets_contig, y_targets_contig
)
native, swap_degrees = _native_co_moment(x_side, y_side)
if native is not None and x_targets.size > 0 and y_targets.size > 0:
first_degree, second_degree = (
(degree_y, degree_x) if swap_degrees else (degree_x, degree_y)
)
native_result = native(
first_degree,
second_degree,
np.ascontiguousarray(x_values),
np.ascontiguousarray(y_values),
np.ascontiguousarray(x_targets),
np.ascontiguousarray(y_targets),
)
moments = np.asarray(native_result, dtype=np.float64).reshape(-1)
if np.asarray(target_x).ndim == 0 and np.asarray(target_y).ndim == 0:
return float(moments[0])
Expand All @@ -115,14 +103,15 @@ def _co_moment(
return moments


def _native_function_available(native: object, x_side: object, y_side: object) -> bool:
def _native_co_moment(x_side: object, y_side: object) -> tuple[Any | None, bool]:
"""Return the native kernel for a side pair and whether it swaps degree order."""
if x_side is _lower and y_side is _lower:
return hasattr(native, "co_lpm_v")
return native_fn("co_lpm_v"), False
if x_side is _upper and y_side is _upper:
return hasattr(native, "co_upm_v")
return native_fn("co_upm_v"), False
if x_side is _upper and y_side is _lower:
return hasattr(native, "d_lpm_v")
return hasattr(native, "d_upm_v")
return native_fn("d_lpm_v"), True
return native_fn("d_upm_v"), False


def _as_pair(
Expand Down
8 changes: 4 additions & 4 deletions src/nns/copula.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import numpy as np
from numpy.typing import NDArray

from nns._native import nnscore
from nns._native import native_fn
from nns.co_moments import _as_pair
from nns.dependence import _dpm_nd
from nns.pm_matrix import pm_matrix
Expand Down Expand Up @@ -48,10 +48,10 @@ def _copula(
target: NDArray[np.float64],
continuous: bool,
) -> float:
native = nnscore()
if native is not None and hasattr(native, "copula_nd"):
native_copula = native_fn("copula_nd")
if native_copula is not None:
return float(
native.copula_nd(
native_copula(
np.ascontiguousarray(np.ravel(values, order="F")),
values.shape[0],
values.shape[1],
Expand Down
Loading
Loading