Skip to content

Commit 86edd1e

Browse files
committed
feat: spec 20 multipole reconstruction in ontos_stream_dump - tag 9, deferred collapse application, dipole/quadrupole expansion
1 parent d5f443a commit 86edd1e

2 files changed

Lines changed: 181 additions & 28 deletions

File tree

‎tools/ontos/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ontos_stream_dump
22

3-
Third independent implementation of the ontos stream spec (normative: ontos repo, `docs/STREAM_SPEC.md`) — Rust sim (ontos), Python verifier (simval), this C++17 std-only re-simulator. Verifies both format versions: v1 life (Phase 0) and v2 gravity (Phase 2: leapfrog + Chebyshev ephemeris windows + momentum ledger, re-simulated bit-exactly with bounded drift reported against an all-fine reference; section 19 Phase 4 collapse/expand: RegionCollapsed totals, monopole gravity, jitter + residual-velocity synthesis). Spike toward the ontos Phase 3 viewer bridge: JoltViewer thin-consumer pattern, zero coupling to the renderer core.
3+
Third independent implementation of the ontos stream spec (normative: ontos repo, `docs/STREAM_SPEC.md`) — Rust sim (ontos), Python verifier (simval), this C++17 std-only re-simulator. Verifies both format versions: v1 life (Phase 0) and v2 gravity (Phase 2: leapfrog + Chebyshev ephemeris windows + momentum ledger, re-simulated bit-exactly with bounded drift reported against an all-fine reference; section 19 Phase 4 collapse/expand: RegionCollapsed totals, monopole gravity, jitter + residual-velocity synthesis; section 20 Phase 4 v2 multipole reconstruction: RegionMultipole dipole/quadrupole totals, dipole-exact position residual + Cholesky quadrupole transform on expansion — streams without RegionMultipole records reconstruct per section 19). Spike toward the ontos Phase 3 viewer bridge: JoltViewer thin-consumer pattern, zero coupling to the renderer core. (`ontos_view` does not parse tag 9 yet.)
44

55
Build: `clang++ -std=c++17 -O2 -ffp-contract=off -o ontos_stream_dump ontos_stream_dump.cpp`
66

‎tools/ontos/ontos_stream_dump.cpp‎

Lines changed: 180 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,14 @@ struct GFit {
347347
// Section 19 collapsed-region state: totals frozen at collapse, membership,
348348
// and the splitmix64 state left after the jitter draws (the expansion spread
349349
// continues drawing from it). Jitter offsets live on the world (indexed by
350-
// body id).
350+
// body id). Section 20 multipole totals (mx, my and the second central
351+
// moments) freeze alongside; mp marks the cycle as section 20.
351352
struct GCollapsed {
352353
bool active = false;
354+
bool mp = false;
353355
u64 n = 0;
354356
f64 mass = 0, com_x = 0, com_y = 0, pxt = 0, pyt = 0, vcx = 0, vcy = 0, energy = 0;
357+
f64 mx = 0, my = 0, qxx = 0, qxy = 0, qyy = 0;
355358
u64 jitter_state = 0;
356359
std::vector<std::size_t> members;
357360
};
@@ -454,6 +457,7 @@ struct GravityWorld {
454457
u64 seed = 0;
455458
u64 tick = 0;
456459
f64 px = 0, py = 0;
460+
bool mp_enabled = true;
457461
std::vector<GBody> bodies;
458462
std::vector<GFit> coarse;
459463
std::vector<u8> body_region;
@@ -670,6 +674,19 @@ struct GravityWorld {
670674
c.vcx = c.pxt / c.mass;
671675
c.vcy = c.pyt / c.mass;
672676
}
677+
// Section 20 multipole totals: the exact dipole accumulators and the
678+
// second central moments about com (id order, left-to-right sums).
679+
c.mx = sum_mx;
680+
c.my = sum_my;
681+
for (std::size_t i : c.members) {
682+
const GBody &b = bodies[i];
683+
const f64 dx = b.x - c.com_x;
684+
const f64 dy = b.y - c.com_y;
685+
c.qxx += b.mass * dx * dx;
686+
c.qxy += b.mass * dx * dy;
687+
c.qyy += b.mass * dy * dy;
688+
}
689+
c.mp = mp_enabled;
673690
SplitMix64 jitter(seed ^ (static_cast<u64>(region) * 0x9E3779B97F4A7C15ULL));
674691
for (std::size_t i : c.members) {
675692
const u64 ux = jitter.draw();
@@ -688,20 +705,91 @@ struct GravityWorld {
688705
return c;
689706
}
690707

691-
// Section 19 expansion: positions from the frozen jitter, velocities
692-
// v_com + spread with the last body absorbing the exact momentum
693-
// residual (bit-identical per the spec).
708+
// Section 19/20 expansion: positions per mode (section 20 synthesizes
709+
// under dipole-exact + quadrupole-transform constraints; section 19 keeps
710+
// com + raw jitter), velocities v_com + spread with the last body
711+
// absorbing the exact momentum residual (bit-identical per the spec).
694712
void expand(int region) {
695713
GCollapsed &c = collapsed[region];
714+
if (c.mp) {
715+
std::vector<std::pair<f64, f64>> base;
716+
base.reserve(c.members.size());
717+
for (std::size_t i : c.members) {
718+
base.emplace_back(body_jx[i], body_jy[i]);
719+
}
720+
if (c.members.size() >= 3) {
721+
f64 swx = 0.0;
722+
f64 swy = 0.0;
723+
for (std::size_t a = 0; a < c.members.size(); ++a) {
724+
const f64 m = bodies[c.members[a]].mass;
725+
swx += m * base[a].first;
726+
swy += m * base[a].second;
727+
}
728+
const f64 wx = swx / c.mass;
729+
const f64 wy = swy / c.mass;
730+
std::vector<std::pair<f64, f64>> dhat(c.members.size());
731+
f64 jxx = 0.0;
732+
f64 jxy = 0.0;
733+
f64 jyy = 0.0;
734+
for (std::size_t a = 0; a < c.members.size(); ++a) {
735+
const f64 m = bodies[c.members[a]].mass;
736+
const f64 dx = base[a].first - wx;
737+
const f64 dy = base[a].second - wy;
738+
jxx += m * dx * dx;
739+
jxy += m * dx * dy;
740+
jyy += m * dy * dy;
741+
dhat[a] = {dx, dy};
742+
}
743+
if (jxx > 0.0 && c.qxx > 0.0) {
744+
const f64 lj00 = std::sqrt(jxx);
745+
const f64 lj10 = jxy / lj00;
746+
const f64 jjd = jyy - lj10 * lj10;
747+
if (jjd > 0.0) {
748+
const f64 lq00 = std::sqrt(c.qxx);
749+
const f64 lq10 = c.qxy / lq00;
750+
const f64 qqd = c.qyy - lq10 * lq10;
751+
if (qqd > 0.0) {
752+
const f64 lj11 = std::sqrt(jjd);
753+
const f64 lq11 = std::sqrt(qqd);
754+
const f64 u00 = 1.0 / lj00;
755+
const f64 u11 = 1.0 / lj11;
756+
const f64 u10 = -(lj10 / (lj00 * lj11));
757+
const f64 a00 = lq00 * u00;
758+
const f64 a11 = lq11 * u11;
759+
const f64 a10 = lq10 * u00 + lq11 * u10;
760+
for (std::size_t a = 0; a < c.members.size(); ++a) {
761+
base[a] = {a00 * dhat[a].first, a10 * dhat[a].first + a11 * dhat[a].second};
762+
}
763+
}
764+
}
765+
}
766+
}
767+
f64 sum_mx = 0.0;
768+
f64 sum_my = 0.0;
769+
for (std::size_t a = 0; a + 1 < c.members.size(); ++a) {
770+
GBody &b = bodies[c.members[a]];
771+
b.x = c.com_x + base[a].first;
772+
b.y = c.com_y + base[a].second;
773+
sum_mx += b.mass * b.x;
774+
sum_my += b.mass * b.y;
775+
}
776+
if (!c.members.empty()) {
777+
const std::size_t last = c.members.back();
778+
bodies[last].x = (c.mx - sum_mx) / bodies[last].mass;
779+
bodies[last].y = (c.my - sum_my) / bodies[last].mass;
780+
}
781+
}
696782
SplitMix64 jitter(c.jitter_state);
697783
f64 sum_mv_x = 0.0;
698784
f64 sum_mv_y = 0.0;
699785
f64 last_mass = 0.0;
700786
for (std::size_t a = 0; a < c.members.size(); ++a) {
701787
const std::size_t i = c.members[a];
702788
GBody &b = bodies[i];
703-
b.x = c.com_x + body_jx[i];
704-
b.y = c.com_y + body_jy[i];
789+
if (!c.mp) {
790+
b.x = c.com_x + body_jx[i];
791+
b.y = c.com_y + body_jy[i];
792+
}
705793
if (a + 1 < c.members.size()) {
706794
const u64 ux = jitter.draw();
707795
const u64 uy = jitter.draw();
@@ -1023,7 +1111,20 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
10231111
int region;
10241112
u8 level;
10251113
};
1114+
struct PendingCollapse {
1115+
u64 t;
1116+
int region;
1117+
u64 n;
1118+
f64 mass, com_x, com_y, pxt, pyt, energy;
1119+
};
1120+
struct PendingMultipole {
1121+
u64 t;
1122+
int region;
1123+
f64 mx, my, qxx, qxy, qyy;
1124+
};
10261125
std::vector<Pending> pending;
1126+
std::vector<PendingCollapse> pending_collapsed;
1127+
std::vector<PendingMultipole> pending_multipole;
10271128
u64 ticks_seen = 0;
10281129
u64 totals_seen = 0;
10291130
u64 states_seen = 0;
@@ -1043,6 +1144,12 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
10431144
std::fprintf(stderr, "error: truncated TickHeader at offset %zu\n", rec_start);
10441145
return 2;
10451146
}
1147+
// Collapse mode is selected per cycle by the presence of the
1148+
// section 20 RegionMultipole record (section 19 streams carry
1149+
// none). RegionLevel records queue and apply here, at the tick
1150+
// boundary, in stream order — level 2 collapses validate against
1151+
// the world's frozen totals bit-for-bit.
1152+
world.mp_enabled = !pending_multipole.empty();
10461153
for (const Pending &p : pending) {
10471154
if (p.level == 0) {
10481155
// Demote on a collapsed region is a no-op (section 19).
@@ -1051,9 +1158,46 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
10511158
}
10521159
} else if (p.level == 1) {
10531160
world.promote(p.region, world.tick + 1);
1161+
} else {
1162+
if (world.rstate[p.region] != 2) {
1163+
world.collapse(p.region, world.tick + 1);
1164+
}
10541165
}
10551166
}
10561167
pending.clear();
1168+
for (const PendingCollapse &pc : pending_collapsed) {
1169+
++collapses_seen;
1170+
const GCollapsed &c = world.collapsed[pc.region];
1171+
if (!c.active || pc.t != world.tick + 1 || pc.n != c.n || !f64_bits_eq(pc.mass, c.mass) ||
1172+
!f64_bits_eq(pc.com_x, c.com_x) || !f64_bits_eq(pc.com_y, c.com_y) ||
1173+
!f64_bits_eq(pc.pxt, c.pxt) || !f64_bits_eq(pc.pyt, c.pyt) ||
1174+
!f64_bits_eq(pc.energy, c.energy)) {
1175+
std::fprintf(stderr,
1176+
"MISMATCH: RegionCollapsed tick=%" PRIu64 " region=%d"
1177+
" stream=(n=%" PRIu64 " mass=%.17g com=(%.17g,%.17g) p=(%.17g,%.17g)"
1178+
" E=%.17g) computed=(n=%" PRIu64 " mass=%.17g com=(%.17g,%.17g)"
1179+
" p=(%.17g,%.17g) E=%.17g)\n",
1180+
pc.t, pc.region, pc.n, pc.mass, pc.com_x, pc.com_y, pc.pxt, pc.pyt,
1181+
pc.energy, c.n, c.mass, c.com_x, c.com_y, c.pxt, c.pyt, c.energy);
1182+
return 1;
1183+
}
1184+
}
1185+
pending_collapsed.clear();
1186+
for (const PendingMultipole &pm : pending_multipole) {
1187+
const GCollapsed &c = world.collapsed[pm.region];
1188+
if (!c.active || !c.mp || pm.t != world.tick + 1 || !f64_bits_eq(pm.mx, c.mx) ||
1189+
!f64_bits_eq(pm.my, c.my) || !f64_bits_eq(pm.qxx, c.qxx) ||
1190+
!f64_bits_eq(pm.qxy, c.qxy) || !f64_bits_eq(pm.qyy, c.qyy)) {
1191+
std::fprintf(stderr,
1192+
"MISMATCH: RegionMultipole tick=%" PRIu64 " region=%d"
1193+
" stream=(m=(%.17g,%.17g) q=(%.17g,%.17g,%.17g))"
1194+
" computed=(m=(%.17g,%.17g) q=(%.17g,%.17g,%.17g))\n",
1195+
pm.t, pm.region, pm.mx, pm.my, pm.qxx, pm.qxy, pm.qyy, c.mx, c.my,
1196+
c.qxx, c.qxy, c.qyy);
1197+
return 1;
1198+
}
1199+
}
1200+
pending_multipole.clear();
10571201
world.step();
10581202
reference.step();
10591203
if (t != world.tick) {
@@ -1116,11 +1260,9 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
11161260
rx, ry, lv, rec_start);
11171261
return 2;
11181262
}
1119-
// Level 2 (collapse) is applied when its RegionCollapsed record
1120-
// arrives; level 0/1 queue for the next tick boundary.
1121-
if (lv != 2) {
1122-
pending.push_back({static_cast<int>(ry) * 2 + static_cast<int>(rx), lv});
1123-
}
1263+
// All levels (including 2/collapse) queue for the next tick
1264+
// boundary, where the collapse's RegionMultipole presence is known.
1265+
pending.push_back({static_cast<int>(ry) * 2 + static_cast<int>(rx), lv});
11241266
} break;
11251267
case 5: {
11261268
u64 t = 0;
@@ -1222,8 +1364,8 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
12221364
}
12231365
} break;
12241366
case 8: {
1225-
// Section 19 RegionCollapsed: apply the collapse at this boundary
1226-
// and validate the record's frozen totals bit-for-bit.
1367+
// Section 19 RegionCollapsed: parsed here, applied and validated
1368+
// bit-for-bit at the tick boundary (see case 1).
12271369
u64 t = 0;
12281370
u32 rx = 0;
12291371
u32 ry = 0;
@@ -1244,22 +1386,33 @@ static int run_gravity(const std::vector<u8> &data, u64 seed, u32 body_count) {
12441386
rx, ry, rec_start);
12451387
return 2;
12461388
}
1247-
++collapses_seen;
1248-
const int region = static_cast<int>(ry) * 2 + static_cast<int>(rx);
1249-
const GCollapsed c = world.collapse(region, world.tick + 1);
1250-
if (t != world.tick + 1 || n != c.n || !f64_bits_eq(mass, c.mass) ||
1251-
!f64_bits_eq(com_x, c.com_x) || !f64_bits_eq(com_y, c.com_y) ||
1252-
!f64_bits_eq(pxt, c.pxt) || !f64_bits_eq(pyt, c.pyt) ||
1253-
!f64_bits_eq(energy, c.energy)) {
1389+
pending_collapsed.push_back(
1390+
{t, static_cast<int>(ry) * 2 + static_cast<int>(rx), n, mass, com_x, com_y, pxt, pyt,
1391+
energy});
1392+
} break;
1393+
case 9: {
1394+
// Section 20 RegionMultipole: parsed here, validated bit-for-bit at
1395+
// the tick boundary.
1396+
u64 t = 0;
1397+
u32 rx = 0;
1398+
u32 ry = 0;
1399+
f64 mx = 0, my = 0, qxx = 0, qxy = 0, qyy = 0;
1400+
if (!take_u64(data, off, t) || !take_u32(data, off, rx) || !take_u32(data, off, ry) ||
1401+
!take_f64(data, off, mx) || !take_f64(data, off, my) ||
1402+
!take_f64(data, off, qxx) || !take_f64(data, off, qxy) ||
1403+
!take_f64(data, off, qyy)) {
1404+
std::fprintf(stderr, "error: truncated RegionMultipole at offset %zu\n", rec_start);
1405+
return 2;
1406+
}
1407+
if (rx > 1 || ry > 1) {
12541408
std::fprintf(stderr,
1255-
"MISMATCH: RegionCollapsed tick=%" PRIu64 " region=(%" PRIu32 ",%" PRIu32
1256-
") stream=(n=%" PRIu64 " mass=%.17g com=(%.17g,%.17g) p=(%.17g,%.17g)"
1257-
" E=%.17g) computed=(n=%" PRIu64 " mass=%.17g com=(%.17g,%.17g)"
1258-
" p=(%.17g,%.17g) E=%.17g)\n",
1259-
t, rx, ry, n, mass, com_x, com_y, pxt, pyt, energy, c.n, c.mass, c.com_x,
1260-
c.com_y, c.pxt, c.pyt, c.energy);
1261-
return 1;
1409+
"error: bad RegionMultipole region (%" PRIu32 ",%" PRIu32
1410+
") at offset %zu\n",
1411+
rx, ry, rec_start);
1412+
return 2;
12621413
}
1414+
pending_multipole.push_back(
1415+
{t, static_cast<int>(ry) * 2 + static_cast<int>(rx), mx, my, qxx, qxy, qyy});
12631416
} break;
12641417
default:
12651418
std::fprintf(stderr, "error: unknown record tag %u at offset %zu\n", tag, rec_start);

0 commit comments

Comments
 (0)