7#include <unordered_map>
102static constexpr double EPS = 1e-10;
104static void assert_near(
double got,
double exp,
const char* what) {
105 if (!std::isfinite(got) || !std::isfinite(exp)) {
106 std::cerr <<
"ASSERT_NEAR failed for " << what <<
" non-finite\n";
110 const double abs_tol = 1e-9;
111 const double rel_tol = 1e-12;
112 const double diff = std::abs(got - exp);
113 const double scale = std::max(1.0, std::abs(exp));
115 if (diff > abs_tol && diff > rel_tol * scale) {
116 std::cerr <<
"ASSERT_NEAR failed for " << what
117 <<
" got=" << got <<
" expected=" <<
exp
118 <<
" diff=" << (got -
exp) <<
"\n";
123static std::shared_ptr<DependentParameter> make_dep_param(
125 const std::unordered_map<
ParamId, std::shared_ptr<Parameter>>& sources,
126 std::function<
void(
const ParamSrc&, std::shared_ptr<DependentParameter>)> recalc)
128 auto dp = std::make_shared<DependentParameter>(
id, sources, recalc);
133static std::shared_ptr<Block> make_base_block(
const std::string& name,
int n_params,
double base)
135 auto b = std::make_shared<Block>();
139 for (
int i = 1; i <= n_params; ++i) {
141 auto p = std::make_shared<Parameter>(pid, base + i, 0.0, 0.0);
142 b->store(
LhaID(i), p);
147static void add_base_dependent_parameter(std::shared_ptr<Block> b,
int base_index_j)
150 const int code = 9000 + base_index_j;
153 std::unordered_map<ParamId, std::shared_ptr<Parameter>> psrc;
157 auto recalc = [](
const ParamSrc& src, std::shared_ptr<DependentParameter> self) {
158 double v1 = 0.0, v2 = 0.0;
160 for (
auto& [pid, p] : src.raw()) {
162 if (pid.code ==
LhaID(1)) v1 = p->get_val();
163 if (pid.code ==
LhaID(2)) v2 = p->get_val();
165 self->set_expected_silent(v1 + 2.0 * v2);
168 auto dp = make_dep_param(myid, psrc, recalc);
169 b->store(
LhaID(code), dp);
172static std::shared_ptr<DependentBlock> make_dep_block(
173 const std::string& name,
174 std::unordered_map<std::string, std::shared_ptr<Block>> sources,
184 auto idx_from_name = [](
const std::string& bn) ->
int {
186 auto pos = bn.find_last_of(
'_');
187 if (pos == std::string::npos)
return 0;
188 return std::stoi(bn.substr(pos + 1));
191 auto recalc = [n_params, width, idx_from_name](
const BlockSrc& src, std::shared_ptr<DependentBlock> self) {
193 std::vector<std::string> names;
194 names.reserve(self->get_source_blocks().size());
195 for (
auto& [bn, _] : self->get_source_blocks()) names.push_back(bn);
196 if (names.size() < 2)
return;
199 const std::string& Aname = names[0];
200 const std::string& Bname = names[1];
201 const int Aj = idx_from_name(Aname) % width;
202 const int Bj = idx_from_name(Bname) % width;
204 const int depA_id = 9000 + Aj;
205 const int depB_id = 9000 + Bj;
207 const double depA = src.get_val(Aname, depA_id);
208 const double depB = src.get_val(Bname, depB_id);
210 for (
int i = 1; i <= n_params; ++i) {
211 const double a = src.get_val(Aname, i);
212 const double b = src.get_val(Bname, i);
213 const double val = a + b + 0.1 * i + 0.001 * (depA + depB);
216 if (!self->contains(
id)) {
218 auto p = std::make_shared<Parameter>(pid, 0.0, 0.0, 0.0);
221 self->retrieve(
id)->set_expected_silent(val);
225 auto db = std::make_shared<DependentBlock>(sources, recalc);
226 db->blockname =
name;
232static void add_depblock_dependent_parameters(
233 std::shared_ptr<DependentBlock> db,
234 std::shared_ptr<Block> A,
235 std::shared_ptr<Block>
B,
241 std::unordered_map<ParamId, std::shared_ptr<Parameter>> psrc;
244 (void)db->retrieve(
LhaID(3));
247 auto recalc = [](
const ParamSrc& src, std::shared_ptr<DependentParameter> self) {
249 for (
auto& [_, p] : src.raw()) if (p)
s += p->get_val();
250 self->set_expected_silent(s);
253 auto dp = make_dep_param(myid, psrc, recalc);
254 db->store(
LhaID(1000 + j_col), dp);
261 std::unordered_map<ParamId, std::shared_ptr<Parameter>> psrc;
265 auto idx_from_name = [](
const std::string& bn) ->
int {
266 auto pos = bn.find_last_of(
'_');
267 if (pos == std::string::npos)
return 0;
268 return std::stoi(bn.substr(pos + 1));
270 const int Aj = idx_from_name(A->blockname);
271 const int Bj = idx_from_name(
B->blockname);
275 (void)db->retrieve(
LhaID(1));
278 auto recalc = [](
const ParamSrc& src, std::shared_ptr<DependentParameter> self) {
280 for (
auto& [_, p] : src.raw()) if (p)
s += p->get_val();
281 self->set_expected_silent(s);
284 auto dp = make_dep_param(myid, psrc, recalc);
285 db->store(
LhaID(9000 + j_col), dp);
294 std::unordered_map<long long, double>
v;
295 static long long key(
int j,
int i) {
296 return (
static_cast<long long>(j) << 32) ^
static_cast<unsigned long long>(i);
298 void set(
int j,
int i,
double val) {
v[
key(j,i)] = val; }
299 double get(
int j,
int i,
double def)
const {
300 auto it =
v.find(
key(j,i));
301 return (it ==
v.end()) ? def : it->second;
305static double expected_base_val(
int base_j,
int i,
const LeafOverride& ovr) {
306 double def = 100.0 * base_j + i;
307 return ovr.
get(base_j, i, def);
310static double expected_base_dp(
int base_j,
const LeafOverride& ovr) {
312 return expected_base_val(base_j, 1, ovr) + 2.0 * expected_base_val(base_j, 2, ovr);
315static double expected_chain_dp(
int d,
int j,
int width,
int nparams,
const LeafOverride& ovr);
317static double expected_block_val(
int d,
int j,
int i,
int width,
int nparams,
const LeafOverride& ovr) {
318 if (d == 0)
return expected_base_val(j, i, ovr);
321 int jb = (j + 1) % width;
323 const double a_i = expected_block_val(d-1, ja, i, width, nparams, ovr);
324 const double b_i = expected_block_val(d-1, jb, i, width, nparams, ovr);
327 const double depA = expected_chain_dp(d-1, ja, width, nparams, ovr);
328 const double depB = expected_chain_dp(d-1, jb, width, nparams, ovr);
330 return a_i + b_i + 0.1 * i + 0.001 * (depA + depB);
333static double expected_mix_dp(
int d,
int j,
int width,
int nparams,
const LeafOverride& ovr) {
336 int jb = (j + 1) % width;
337 const double a1 = expected_block_val(d-1, ja, 1, width, nparams, ovr);
338 const double b2 = expected_block_val(d-1, jb, 2, width, nparams, ovr);
339 const double self3 = expected_block_val(d, j, 3, width, nparams, ovr);
340 return a1 + b2 + self3;
343static double expected_chain_dp(
int d,
int j,
int width,
int nparams,
const LeafOverride& ovr) {
347 if (d == 0)
return expected_base_dp(j, ovr);
349 int jb = (j + 1) % width;
350 const double depA = expected_chain_dp(d-1, ja, width, nparams, ovr);
351 const double depB = expected_chain_dp(d-1, jb, width, nparams, ovr);
352 const double self1 = expected_block_val(d, j, 1, width, nparams, ovr);
353 return depA + depB + self1;
364 std::vector<std::vector<std::shared_ptr<Block>>>
levels;
367static Graph build_graph(
int width,
int depth,
int nparams)
374 g.levels.emplace_back();
375 for (
int j = 0; j < width; ++j) {
376 auto b = make_base_block(
"B0_" + std::to_string(j), nparams, 100.0 * j);
377 add_base_dependent_parameter(b, j);
378 g.levels[0].push_back(b);
381 for (
int d = 1;
d <= depth; ++
d) {
382 g.levels.emplace_back();
383 for (
int j = 0; j < width; ++j) {
384 auto A =
g.levels[
d-1][j];
385 auto B =
g.levels[
d-1][(j + 1) % width];
387 std::unordered_map<std::string, std::shared_ptr<Block>> src;
388 src[A->blockname] = A;
389 src[
B->blockname] =
B;
391 auto db = make_dep_block(
"D" + std::to_string(d) +
"_" + std::to_string(j), src, nparams, width);
394 add_depblock_dependent_parameters(db, A,
B, j);
396 g.levels[
d].push_back(db);
406static void check_many_values(
const Graph&
g,
const LeafOverride& ovr, std::mt19937_64& rng)
408 std::uniform_int_distribution<int> ddist(0,
g.depth);
409 std::uniform_int_distribution<int> jdist(0,
g.width - 1);
410 std::uniform_int_distribution<int> idist(1,
g.nparams);
413 for (
int t = 0; t < 400; ++t) {
414 int d = ddist(rng), j = jdist(rng), i = idist(rng);
415 auto b =
g.levels[
d][j];
417 double got = b->retrieve(
LhaID(i))->get_val();
418 double exp = expected_block_val(d, j, i,
g.width,
g.nparams, ovr);
420 std::string what =
"block[" + std::to_string(d) +
"][" + std::to_string(j) +
"].param[" + std::to_string(i) +
"]";
421 assert_near(got, exp, what.c_str());
425 std::uniform_int_distribution<int> ddist_dep(1,
g.depth);
426 for (
int t = 0; t < 250; ++t) {
427 int d = ddist_dep(rng), j = jdist(rng);
428 auto b =
g.levels[
d][j];
430 double got = b->retrieve(
LhaID(1000 + j))->get_val();
431 double exp = expected_mix_dp(d, j,
g.width,
g.nparams, ovr);
433 std::string what =
"block[" + std::to_string(d) +
"][" + std::to_string(j) +
"].MIX_DP";
434 assert_near(got, exp, what.c_str());
438 for (
int t = 0; t < 250; ++t) {
439 int d = ddist(rng), j = jdist(rng);
440 auto b =
g.levels[
d][j];
442 double got = b->retrieve(
LhaID(9000 + j))->get_val();
443 double exp = expected_chain_dp(d, j,
g.width,
g.nparams, ovr);
445 std::string what =
"block[" + std::to_string(d) +
"][" + std::to_string(j) +
"].CHAIN_DP";
446 assert_near(got, exp, what.c_str());
450static void touch_in_weird_order(
const Graph&
g)
453 for (
int d =
g.depth;
d >= 0; --
d) {
454 for (
int j = 0; j <
g.width; ++j) {
455 auto b =
g.levels[
d][j];
456 (void)b->retrieve(
LhaID(9000 + j))->get_val();
457 (void)b->retrieve(
LhaID(1))->get_val();
458 (void)b->retrieve(
LhaID(3))->get_val();
459 if (d > 0) (void)b->retrieve(
LhaID(1000 + j))->get_val();
464static void apply_leaf_updates(
Graph&
g,
LeafOverride& ovr,
const std::vector<std::tuple<int,int,double>>& ops)
466 for (
auto& [j,i,v] : ops) {
467 g.levels[0][j]->assign(
LhaID(i), v);
474 auto root =
g.levels[
g.depth][0];
475 double before = root->retrieve(
LhaID(1))->get_val();
478 g.levels[0][0]->assign(
LhaID(1), 424242.0);
479 ovr.
set(0, 1, 424242.0);
481 double still = root->retrieve(
LhaID(1))->get_val();
482 assert_near(still, before,
"freeze: root[1] cached");
485 double after = root->retrieve(
LhaID(1))->get_val();
486 double exp = expected_block_val(
g.depth, 0, 1,
g.width,
g.nparams, ovr);
487 assert_near(after, exp,
"unfreeze: root[1] recompute");
497 auto db = std::dynamic_pointer_cast<DependentBlock>(
g.levels[d][j]);
498 if (!db) { std::cerr <<
"ERROR: expected DependentBlock\n"; std::abort(); }
500 auto dp = std::dynamic_pointer_cast<DependentParameter>(db->retrieve(
LhaID(1000 + j)));
501 if (!dp) { std::cerr <<
"ERROR: expected DependentParameter\n"; std::abort(); }
503 auto A =
g.levels[
d-1][j];
504 auto B =
g.levels[
d-1][(j+1)%
g.width];
506 std::unordered_map<ParamId, std::shared_ptr<Parameter>> new_sources;
511 auto new_lambda = [](
const ParamSrc& src, std::shared_ptr<DependentParameter> self) {
513 for (
auto& [_, p] : src.raw()) if (p)
s += p->get_val();
514 self->set_expected_silent(s);
517 double oldv = dp->get_val();
518 dp->rebind(new_sources, new_lambda);
521 g.levels[0][2]->assign(
LhaID(4), 123456.0);
522 ovr.
set(2, 4, 123456.0);
524 const double expA4 = expected_block_val(d-1, j, 4,
g.width,
g.nparams, ovr);
525 const double expB5 = expected_block_val(d-1, (j+1)%
g.width, 5,
g.width,
g.nparams, ovr);
526 const double expAchain = expected_chain_dp(d-1, j,
g.width,
g.nparams, ovr);
527 const double exp = expA4 + expB5 + expAchain;
529 double newv = dp->get_val();
530 if (std::abs(newv - oldv) < 1e-12) {
531 std::cerr <<
"ERROR: rebind dp did not change\n";
534 assert_near(newv, exp,
"rebind dp expected");
537static void microbench(
Graph&
g,
LeafOverride& ovr, std::mt19937_64& rng,
bool verbose)
539 using clock = std::chrono::high_resolution_clock;
540 using ns = std::chrono::nanoseconds;
542 auto root =
g.levels[
g.depth][0];
545 for (
int k = 0; k < 200; ++k) {
546 (void)root->retrieve(
LhaID(1))->get_val();
547 (void)root->retrieve(
LhaID(9000 + 0))->get_val();
548 (void)root->retrieve(
LhaID(1000 + 0))->get_val();
551 std::uniform_int_distribution<int> jdist(0,
g.width - 1);
552 std::uniform_int_distribution<int> idist(1,
g.nparams);
553 std::uniform_real_distribution<double> vdist(-1e4, 1e4);
558 auto t0 = clock::now();
560 for (
int t = 0; t < N; ++t) {
563 double nv = vdist(rng);
565 g.levels[0][bj]->assign(
LhaID(pi), nv);
569 sink += root->retrieve(
LhaID(1))->get_val();
570 sink += root->retrieve(
LhaID(9000 + 0))->get_val();
571 sink += root->retrieve(
LhaID(1000 + 0))->get_val();
573 if (verbose && (t % 200 == 0)) {
574 std::cout <<
"[bench] t=" << t <<
" updated B0_" << bj <<
"[" << pi <<
"]=" << nv
575 <<
" -> root[1]=" << root->retrieve(
LhaID(1))->get_val() <<
"\n";
578 auto t1 = clock::now();
579 auto dt_update_read = std::chrono::duration_cast<ns>(t1 - t0).count();
582 auto r0 = clock::now();
583 for (
int t = 0; t < N * 5; ++t) {
584 sink += root->retrieve(
LhaID(1))->get_val();
585 sink += root->retrieve(
LhaID(9000 + 0))->get_val();
586 sink += root->retrieve(
LhaID(1000 + 0))->get_val();
588 auto r1 = clock::now();
589 auto dt_reads = std::chrono::duration_cast<ns>(r1 - r0).count();
591 std::cout <<
"\n==== Microbench (rough signal) ====\n";
592 std::cout <<
"Pattern A: (update leaf -> read root[1]+root[CHAIN_DP]+root[MIX_DP]) x " << N <<
"\n";
593 std::cout <<
" total: " << (dt_update_read / 1e6) <<
" ms"
594 <<
" | avg: " << (dt_update_read / (
double)N) <<
" ns/iter\n";
595 std::cout <<
"Pattern B: (read root trio) x " << (N*5) <<
" (no updates)\n";
596 std::cout <<
" total: " << (dt_reads / 1e6) <<
" ms"
597 <<
" | avg: " << (dt_reads / (
double)(N*5)) <<
" ns/iter\n";
598 std::cout <<
"sink=" << sink <<
" (ignore)\n\n";
603 constexpr bool VERBOSE =
true;
605 constexpr int WIDTH = 6;
606 constexpr int DEPTH = 8;
607 constexpr int NPARAMS = 30;
609 std::mt19937_64 rng(123456789ULL);
611 std::cout <<
"Building graph (WIDTH=" << WIDTH <<
", DEPTH=" << DEPTH <<
", NPARAMS=" << NPARAMS <<
")...\n";
612 Graph g = build_graph(WIDTH, DEPTH, NPARAMS);
616 std::cout <<
"Dependency pattern:\n";
617 std::cout <<
" D(d,j) depends on A=level[d-1][j], B=level[d-1][(j+1)%WIDTH]\n";
618 std::cout <<
" D(d,j)[i] = A[i] + B[i] + 0.1*i + 0.001*(A[CHAIN_DP] + B[CHAIN_DP])\n";
619 std::cout <<
" BASE CHAIN_DP in B0_j: B0_j[1] + 2*B0_j[2]\n";
620 std::cout <<
" MIX_DP in each D(d,j): A[1] + B[2] + D(d,j)[3]\n";
621 std::cout <<
" CHAIN_DP in each D(d,j): A[CHAIN_DP] + B[CHAIN_DP] + D(d,j)[1]\n\n";
624 std::cout <<
"Test 1: initial correctness (random sampling)...\n";
625 check_many_values(
g, ovr, rng);
628 std::cout <<
"Test 2: weird read order then correctness...\n";
629 touch_in_weird_order(
g);
630 check_many_values(
g, ovr, rng);
633 std::cout <<
"Test 3: single-leaf update then checks...\n";
634 apply_leaf_updates(
g, ovr, { {2, 1, 9999.0} });
636 auto root =
g.levels[
g.depth][0];
637 double got_root_1 = root->retrieve(
LhaID(1))->get_val();
638 double exp_root_1 = expected_block_val(
g.depth, 0, 1,
g.width,
g.nparams, ovr);
639 assert_near(got_root_1, exp_root_1,
"target: root[1] after leaf update");
641 double got_root_chain = root->retrieve(
LhaID(9000 + 0))->get_val();
642 double exp_root_chain = expected_chain_dp(
g.depth, 0,
g.width,
g.nparams, ovr);
643 assert_near(got_root_chain, exp_root_chain,
"target: root CHAIN_DP after leaf update");
645 double got_root_mix = root->retrieve(
LhaID(1000 + 0))->get_val();
646 double exp_root_mix = expected_mix_dp(
g.depth, 0,
g.width,
g.nparams, ovr);
647 assert_near(got_root_mix, exp_root_mix,
"target: root MIX_DP after leaf update");
649 check_many_values(
g, ovr, rng);
652 std::cout <<
"Test 4: batch updates before reads...\n";
654 std::vector<std::tuple<int,int,double>> ops;
655 std::uniform_int_distribution<int> jdist(0,
g.width - 1);
656 std::uniform_int_distribution<int> idist(1,
g.nparams);
657 std::uniform_real_distribution<double> vdist(-1e4, 1e4);
659 for (
int t = 0; t < 40; ++t) ops.emplace_back(jdist(rng), idist(rng), vdist(rng));
660 apply_leaf_updates(
g, ovr, ops);
663 auto mid =
g.levels[
g.depth/2][3];
664 double got_mid_chain = mid->retrieve(
LhaID(9000 + 3))->get_val();
665 double exp_mid_chain = expected_chain_dp(
g.depth/2, 3,
g.width,
g.nparams, ovr);
666 assert_near(got_mid_chain, exp_mid_chain,
"batch: mid CHAIN_DP");
668 double got_root_3 = root->retrieve(
LhaID(3))->get_val();
669 double exp_root_3 = expected_block_val(
g.depth, 0, 3,
g.width,
g.nparams, ovr);
670 assert_near(got_root_3, exp_root_3,
"batch: root[3]");
672 check_many_values(
g, ovr, rng);
676 std::cout <<
"Test 5: freeze/unfreeze...\n";
677 test_freeze_unfreeze(
g, ovr);
680 std::cout <<
"Test 6: DependentParameter::rebind...\n";
681 test_rebind_dependent_parameter(
g, ovr);
684 std::cout <<
"Bench: timing signal...\n";
685 microbench(
g, ovr, rng, VERBOSE);
687 std::cout <<
"ALL TESTS PASSED ✅\n";
Defines classes used to store parameters and to build derived/dependent parameter blocks.
Defines parameters whose values are lazily computed from other parameters.
Lightweight view over a set of source blocks.
Lightweight view over a set of source parameters keyed by ParamId.
std::vector< std::vector< std::shared_ptr< Block > > > levels
void set(int j, int i, double val)
double get(int j, int i, double def) const
std::unordered_map< long long, double > v
static long long key(int j, int i)
Represents an identifier of a LHA element, possibly containing several sub-ids.
Composite identifier for a single parameter.