|
4 | 4 | #include <nanobind/stl/vector.h> |
5 | 5 |
|
6 | 6 | #include <algorithm> |
| 7 | +#include <climits> |
7 | 8 | #include <cmath> |
8 | 9 | #include <cstdint> |
9 | 10 | #include <cstddef> |
10 | 11 | #include <limits> |
| 12 | +#include <map> |
11 | 13 | #include <stdexcept> |
12 | 14 | #include <string> |
13 | 15 | #include <vector> |
@@ -360,6 +362,173 @@ double gravity_exact_impl(const double* data, std::size_t raw_n) { |
360 | 362 | return 0.25 * (q2 + mode_gravity + mean + 0.5 * (q1 + q3)); |
361 | 363 | } |
362 | 364 |
|
| 365 | +// Bit-faithful port of the pure-Python NNS.part recursive partitioner |
| 366 | +// (`nns.part.nns_part`) for the noise_reduction="off" path that the NNS.reg |
| 367 | +// numeric regression / nns_arma hot path uses exclusively. Centers and |
| 368 | +// regression points are aggregated with gravity_exact, matching the Python |
| 369 | +// `_gravity`. The caller (Python wrapper) resolves max_order exactly as |
| 370 | +// nns_part does and falls back to pure Python for the mean/median/mode paths, |
| 371 | +// so this only implements the gravity ("off") aggregation. |
| 372 | +nb::dict nns_part_off(const Vector& xa, const Vector& ya, bool xonly, int max_order, int obs_req, |
| 373 | + bool min_obs_stop) { |
| 374 | + const std::size_t n = checked_size(xa, "x"); |
| 375 | + if (ya.shape(0) != n) { |
| 376 | + throw std::invalid_argument("x and y must have the same length."); |
| 377 | + } |
| 378 | + const double* x = xa.data(); |
| 379 | + const double* y = ya.data(); |
| 380 | + |
| 381 | + // floor_order = floor(log2(max(1, n))), computed exactly via bit position. |
| 382 | + int floor_order = 0; |
| 383 | + { |
| 384 | + std::size_t v = (n < 1U) ? 1U : n; |
| 385 | + while ((static_cast<std::size_t>(1) << (floor_order + 1)) <= v) { |
| 386 | + ++floor_order; |
| 387 | + } |
| 388 | + } |
| 389 | + if (max_order == 0) { |
| 390 | + max_order = 1; |
| 391 | + } |
| 392 | + |
| 393 | + std::vector<std::string> quad(n, "q"); |
| 394 | + std::vector<std::string> prior(n, "pq"); |
| 395 | + int depth = 0; |
| 396 | + |
| 397 | + while (depth < max_order && depth < floor_order) { |
| 398 | + // np.unique(quad) -> sorted labels, inverse, counts (std::map sorts keys). |
| 399 | + std::map<std::string, int> label_idx; |
| 400 | + for (const std::string& s : quad) { |
| 401 | + label_idx.emplace(s, 0); |
| 402 | + } |
| 403 | + int g = 0; |
| 404 | + std::vector<std::string> labels; |
| 405 | + labels.reserve(label_idx.size()); |
| 406 | + for (auto& kv : label_idx) { |
| 407 | + kv.second = g++; |
| 408 | + labels.push_back(kv.first); |
| 409 | + } |
| 410 | + const int ngroups = g; |
| 411 | + std::vector<int> counts(ngroups, 0); |
| 412 | + std::vector<std::vector<std::size_t>> members(ngroups); |
| 413 | + for (std::size_t i = 0; i < n; ++i) { |
| 414 | + const int gi = label_idx[quad[i]]; |
| 415 | + counts[gi] += 1; |
| 416 | + members[gi].push_back(i); |
| 417 | + } |
| 418 | + |
| 419 | + bool any_split = false; |
| 420 | + for (int gi = 0; gi < ngroups; ++gi) { |
| 421 | + if (counts[gi] <= obs_req) { |
| 422 | + continue; |
| 423 | + } |
| 424 | + any_split = true; |
| 425 | + std::vector<double> gx; |
| 426 | + std::vector<double> gy; |
| 427 | + gx.reserve(members[gi].size()); |
| 428 | + gy.reserve(members[gi].size()); |
| 429 | + for (const std::size_t i : members[gi]) { |
| 430 | + gx.push_back(x[i]); |
| 431 | + gy.push_back(y[i]); |
| 432 | + } |
| 433 | + const double cx = gravity_exact_impl(gx.data(), gx.size()); |
| 434 | + const double cy = xonly ? 0.0 : gravity_exact_impl(gy.data(), gy.size()); |
| 435 | + for (const std::size_t i : members[gi]) { |
| 436 | + prior[i] = labels[gi]; |
| 437 | + if (xonly) { |
| 438 | + const bool low_x = std::isfinite(x[i]) && std::isfinite(cx) && (x[i] > cx); |
| 439 | + quad[i] += (low_x ? '2' : '1'); |
| 440 | + } else { |
| 441 | + const bool low_x = std::isfinite(x[i]) && std::isfinite(cx) && (x[i] <= cx); |
| 442 | + const bool low_y = std::isfinite(y[i]) && std::isfinite(cy) && (y[i] <= cy); |
| 443 | + const int qn = 1 + (low_x ? 1 : 0) + 2 * (low_y ? 1 : 0); |
| 444 | + quad[i] += static_cast<char>('0' + qn); |
| 445 | + } |
| 446 | + } |
| 447 | + } |
| 448 | + if (!any_split) { |
| 449 | + break; |
| 450 | + } |
| 451 | + ++depth; |
| 452 | + |
| 453 | + if (min_obs_stop) { |
| 454 | + std::map<std::string, int> post_counts; |
| 455 | + for (const std::string& s : quad) { |
| 456 | + post_counts[s] += 1; |
| 457 | + } |
| 458 | + int min_count = INT_MAX; |
| 459 | + for (const auto& kv : post_counts) { |
| 460 | + min_count = std::min(min_count, kv.second); |
| 461 | + } |
| 462 | + if (min_count <= obs_req) { |
| 463 | + break; |
| 464 | + } |
| 465 | + } |
| 466 | + } |
| 467 | + |
| 468 | + // regression points grouped by prior.quadrant (sorted labels via std::map). |
| 469 | + std::map<std::string, std::vector<std::size_t>> prior_groups; |
| 470 | + for (std::size_t i = 0; i < n; ++i) { |
| 471 | + prior_groups[prior[i]].push_back(i); |
| 472 | + } |
| 473 | + std::vector<std::string> rp_quadrant; |
| 474 | + std::vector<double> rp_x; |
| 475 | + std::vector<double> rp_y; |
| 476 | + rp_quadrant.reserve(prior_groups.size()); |
| 477 | + rp_x.reserve(prior_groups.size()); |
| 478 | + rp_y.reserve(prior_groups.size()); |
| 479 | + for (const auto& kv : prior_groups) { |
| 480 | + std::vector<double> gx; |
| 481 | + std::vector<double> gy; |
| 482 | + gx.reserve(kv.second.size()); |
| 483 | + gy.reserve(kv.second.size()); |
| 484 | + for (const std::size_t i : kv.second) { |
| 485 | + gx.push_back(x[i]); |
| 486 | + gy.push_back(y[i]); |
| 487 | + } |
| 488 | + rp_quadrant.push_back(kv.first); |
| 489 | + rp_x.push_back(gravity_exact_impl(gx.data(), gx.size())); |
| 490 | + rp_y.push_back(gravity_exact_impl(gy.data(), gy.size())); |
| 491 | + } |
| 492 | + |
| 493 | + // _is_discrete_like_r(x) -> round regression-point x half-up. |
| 494 | + bool any_finite = false; |
| 495 | + bool all_integral = true; |
| 496 | + for (std::size_t i = 0; i < n; ++i) { |
| 497 | + if (std::isfinite(x[i])) { |
| 498 | + any_finite = true; |
| 499 | + if (x[i] != std::floor(x[i])) { |
| 500 | + all_integral = false; |
| 501 | + break; |
| 502 | + } |
| 503 | + } |
| 504 | + } |
| 505 | + if (any_finite && all_integral) { |
| 506 | + for (double& v : rp_x) { |
| 507 | + const double f = std::floor(v); |
| 508 | + v = (v - f < 0.5) ? f : std::ceil(v); |
| 509 | + } |
| 510 | + } |
| 511 | + |
| 512 | + std::vector<double> dt_x(x, x + n); |
| 513 | + std::vector<double> dt_y(y, y + n); |
| 514 | + nb::dict dt; |
| 515 | + dt["x"] = std::move(dt_x); |
| 516 | + dt["y"] = std::move(dt_y); |
| 517 | + dt["quadrant"] = std::move(quad); |
| 518 | + dt["prior.quadrant"] = std::move(prior); |
| 519 | + |
| 520 | + nb::dict regression_points; |
| 521 | + regression_points["quadrant"] = std::move(rp_quadrant); |
| 522 | + regression_points["x"] = std::move(rp_x); |
| 523 | + regression_points["y"] = std::move(rp_y); |
| 524 | + |
| 525 | + nb::dict out; |
| 526 | + out["order"] = depth; |
| 527 | + out["dt"] = dt; |
| 528 | + out["regression.points"] = regression_points; |
| 529 | + return out; |
| 530 | +} |
| 531 | + |
363 | 532 | } // namespace |
364 | 533 |
|
365 | 534 | NB_MODULE(_nnscore, m) { |
@@ -494,4 +663,6 @@ NB_MODULE(_nnscore, m) { |
494 | 663 | m.def("gravity_exact", [](const Vector& x) { |
495 | 664 | return gravity_exact_impl(x.data(), x.shape(0)); |
496 | 665 | }); |
| 666 | + m.def("nns_part_off", &nns_part_off, nb::arg("x"), nb::arg("y"), nb::arg("xonly"), |
| 667 | + nb::arg("max_order"), nb::arg("obs_req"), nb::arg("min_obs_stop")); |
497 | 668 | } |
0 commit comments