Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
layout_tests.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // White-box layout checks for the dense profiles. This file includes the |
| 2 | // core translation unit so it can inspect site geometry directly; it is |
| 3 | // built as its own executable and does not link ckey_core. |
| 4 | "../src/ckey.cpp" |
| 5 | "check.h" |
| 6 | "svg_raster.h" |
| 7 | |
| 8 | <cstdio> |
| 9 | |
| 10 | namespace { |
| 11 | |
| 12 | // Filled from the reference implementation; see the format specifications. |
| 13 | constexpr uint64_t kTextLayoutKat = 0x17a92d978ee7b3cfull, kExtendedLayoutKat = 0x762d03b44db9c502ull; |
| 14 | |
| 15 | std::string render_text(const std::vector<uint8_t> &payload) { |
| 16 | ckey_text_symbol s{}; ckey_diagnostics d{}; size_t n = 0; |
| 17 | CKEY_CHECK(ckey_encode_text(payload.data(), payload.size(), &s, &d) == CKEY_OK); |
| 18 | ckey_render_text_svg(&s, 8, nullptr, 0, &n); std::string svg(n, '\0'); |
| 19 | CKEY_CHECK(ckey_render_text_svg(&s, 8, svg.data(), n, &n) == CKEY_OK); svg.resize(n - 1); return svg; |
| 20 | } |
| 21 | |
| 22 | std::string render_extended(const std::vector<uint8_t> &payload) { |
| 23 | ckey_extended_symbol s{}; ckey_diagnostics d{}; size_t n = 0; |
| 24 | CKEY_CHECK(ckey_encode_extended(payload.data(), payload.size(), &s, &d) == CKEY_OK); |
| 25 | ckey_render_extended_svg(&s, 8, nullptr, 0, &n); std::string svg(n, '\0'); |
| 26 | CKEY_CHECK(ckey_render_extended_svg(&s, 8, svg.data(), n, &n) == CKEY_OK); svg.resize(n - 1); return svg; |
| 27 | } |
| 28 | |
| 29 | std::vector<uint8_t> pseudo_random(size_t n, uint32_t seed) { |
| 30 | std::vector<uint8_t> out(n); |
| 31 | for (auto &b : out) { seed = seed * 1664525u + 1013904223u; b = static_cast<uint8_t>(seed >> 24); } |
| 32 | return out; |
| 33 | } |
| 34 | |
| 35 | // Every candidate lattice site must be decided with slack: no implementation |
| 36 | // whose sqrt/sin/cos differ in the last place may derive another site set. |
| 37 | void check_margin_stability(const DenseGeometry &g, int grid, double pitch, double documented, const char *label) { |
| 38 | double closest = 1e9; |
| 39 | for (int row = 0; row < grid; ++row) for (int col = 0; col < grid; ++col) |
| 40 | closest = std::min(closest, std::abs(dense_site_margin(g, (col + .5) * pitch, (row + .5) * pitch))); |
| 41 | std::printf("%s: closest site-to-clearance slack %.6f units\n", label, closest); |
| 42 | CKEY_CHECK(closest > 1e-6 && closest >= documented); // the spec states this bound |
| 43 | } |
| 44 | |
| 45 | struct Clearances { double frame, finder, grid; double inner_radius, center; int px_per_unit, units, margin; DenseGeometry g; }; |
| 46 | |
| 47 | // Measures, on the rendered SVG, (a) the minimum gap between payload ink and |
| 48 | // the frame, finders, and Grid Key, and (b) the largest distance from any |
| 49 | // point of the payload field to the nearest payload ink ("dead space"). |
| 50 | void check_rendered_layout(const std::string &svg_text, const Clearances &c, double dead_limit, const char *label) { |
| 51 | const auto svg = svgtest::parse(svg_text); |
| 52 | const int side = c.units * c.px_per_unit; const double unit = c.px_per_unit; |
| 53 | const auto mask = [&](const char *layer) { |
| 54 | auto px = svgtest::rasterize(svg, side, side, 1, 0, 1, layer); return px; |
| 55 | }; |
| 56 | const auto data = mask("ckey-data"); |
| 57 | const auto frame = svgtest::distance_to_set(mask("ckey-frame"), side, side); |
| 58 | const auto finders = svgtest::distance_to_set(mask("ckey-finders"), side, side); |
| 59 | const auto grid = svgtest::distance_to_set(mask("ckey-grid-key"), side, side); |
| 60 | const auto to_data = svgtest::distance_to_set(data, side, side); |
| 61 | const auto cartesian = mask("ckey-data-cartesian"); |
| 62 | const auto polar_keys = svgtest::distance_to_set(mask("ckey-polar-keys"), side, side); |
| 63 | const auto polar_data = svgtest::distance_to_set(mask("ckey-data-polar"), side, side); |
| 64 | double gap_frame = 1e9, gap_finder = 1e9, gap_grid = 1e9, gap_polar = 1e9, dead = 0, dead_x = 0, dead_y = 0; |
| 65 | for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) { |
| 66 | const size_t i = static_cast<size_t>(y) * side + x; |
| 67 | if (cartesian[i]) gap_polar = std::min(gap_polar, std::min(polar_keys[i], polar_data[i]) / unit); |
| 68 | if (data[i]) { |
| 69 | gap_frame = std::min(gap_frame, frame[i] / unit); |
| 70 | gap_finder = std::min(gap_finder, finders[i] / unit); |
| 71 | gap_grid = std::min(gap_grid, grid[i] / unit); |
| 72 | continue; |
| 73 | } |
| 74 | // Payload field: inside the frame, outside every declared clearance |
| 75 | // and the central quiet zone. |
| 76 | const double ux = (x + .5) / unit - c.margin, uy = (y + .5) / unit - c.margin; |
| 77 | const double r = std::hypot(ux - c.center, uy - c.center); |
| 78 | if (rounded_square_sd(ux, uy, c.g.frame_center, c.g.frame_half, c.g.frame_radius) > 0 || r < c.inner_radius) continue; |
| 79 | if (frame[i] / unit < c.frame + 0.5 || finders[i] / unit < c.finder + 0.5 || grid[i] / unit < c.grid + 0.5) continue; |
| 80 | // The top-left finder's split gap is the orientation mark: structural |
| 81 | // clear space, measured against the complete arc. |
| 82 | if (finder_centerline_distance(ux, uy, c.g.finder_inset, c.g.finder_inset, c.g.finder_radius, false, false) < |
| 83 | c.g.finder_half_stroke + c.finder + 0.5) continue; |
| 84 | if (to_data[i] / unit > dead) { dead = to_data[i] / unit; dead_x = ux; dead_y = uy; } |
| 85 | } |
| 86 | const double tolerance = 1.5 / unit; // point-sampled mask quantization |
| 87 | std::printf("%s: gaps frame %.3f finders %.3f grid %.3f radial %.3f; largest payload-free distance %.3f units at (%.1f, %.1f)\n", |
| 88 | label, gap_frame, gap_finder, gap_grid, gap_polar, dead, dead_x, dead_y); |
| 89 | CKEY_CHECK(gap_frame >= c.frame - tolerance); |
| 90 | CKEY_CHECK(gap_finder >= c.finder - tolerance); |
| 91 | CKEY_CHECK(gap_grid >= c.grid - tolerance); |
| 92 | CKEY_CHECK(gap_polar >= 0.5 - tolerance); // Cartesian ink vs Ray Keys and the radial alphabet |
| 93 | CKEY_CHECK(dead <= dead_limit); |
| 94 | } |
| 95 | |
| 96 | // The codeword must be spread over the symbol, not packed into one region: |
| 97 | // each of the four quadrants (about the center) carries a near-proportional |
| 98 | // share of codeword-bearing Cartesian sites. |
| 99 | template <class NodeT, class IsCart> |
| 100 | void check_spread(const std::vector<NodeT> &nodes, size_t bits, double center, IsCart is_cart, const char *label) { |
| 101 | std::array<size_t, 4> all{}, data{}; |
| 102 | for (size_t i = 0; i < nodes.size(); ++i) { |
| 103 | if (!is_cart(nodes[i])) continue; |
| 104 | const int q = (nodes[i].x >= center ? 1 : 0) + (nodes[i].y >= center ? 2 : 0); |
| 105 | ++all[q]; if (i < bits) ++data[q]; |
| 106 | } |
| 107 | for (int q = 0; q < 4; ++q) { |
| 108 | const double share = static_cast<double>(data[q]) / all[q]; |
| 109 | std::printf("%s quadrant %d: %zu of %zu Cartesian sites carry codeword bits (%.2f)\n", label, q, data[q], all[q], share); |
| 110 | } |
| 111 | const double expected = static_cast<double>(data[0] + data[1] + data[2] + data[3]) / (all[0] + all[1] + all[2] + all[3]); |
| 112 | for (int q = 0; q < 4; ++q) CKEY_CHECK(std::abs(static_cast<double>(data[q]) / all[q] - expected) < 0.05); |
| 113 | } |
| 114 | |
| 115 | } // namespace |
| 116 | |
| 117 | int main() { |
| 118 | // ---- Site sets (normative counts for Format 0.25 / 0.26) ---- |
| 119 | const auto &text = text_nodes(); |
| 120 | const auto &extended = extended_nodes(); |
| 121 | size_t text_cart = 0, ext_cart = 0, ext_ring = 0, ext_ray = 0; |
| 122 | for (const auto &n : text) text_cart += n.cart; |
| 123 | for (const auto &n : extended) { ext_cart += n.kind == ExtendedKind::cart; ext_ring += n.kind == ExtendedKind::ring; ext_ray += n.kind == ExtendedKind::ray; } |
| 124 | std::printf("Profile 2 sites: %zu total (%zu Cartesian), codeword bits %zu\n", text.size(), text_cart, kTextCodewordBytes * 8); |
| 125 | std::printf("Profile 3 sites: %zu total (%zu Cartesian, %zu ring, %zu ray components), codeword bits %zu\n", |
| 126 | extended.size(), ext_cart, ext_ring, ext_ray, kExtendedBits); |
| 127 | CKEY_CHECK(text.size() == kTextSiteCount && text_cart == kTextCartesianSites); |
| 128 | CKEY_CHECK(extended.size() == kExtendedSiteCount && ext_cart == kExtendedCartesianSites); |
| 129 | CKEY_CHECK(ext_ray == 573 * 3 && ext_ring == 9682); |
| 130 | CKEY_CHECK(text_whitening().size() == text.size() && extended_whitening().size() == extended.size()); |
| 131 | check_margin_stability(text_geometry(), kTextGrid, 0.5, 1.8e-2, "Profile 2"); |
| 132 | check_margin_stability(extended_geometry(), kExtendedGrid, 0.5, 3e-4, "Profile 3"); |
| 133 | check_spread(text, kTextCodewordBytes * 8, kCenter, [](const TextNode &n) { return n.cart; }, "Profile 2"); |
| 134 | check_spread(extended, kExtendedBits, 58.0, [](const ExtendedNode &n) { return n.kind == ExtendedKind::cart; }, "Profile 3"); |
| 135 | |
| 136 | // Each site kind carries a proportional share of the codeword. (Before |
| 137 | // Format 0.26 only 173 of 573 rays carried data and the rest were blank |
| 138 | // stubs; Format 0.25/0.26 fill sites are whitened, so none look empty.) |
| 139 | { |
| 140 | std::array<size_t, 3> all{}, data{}; |
| 141 | for (size_t i = 0; i < extended.size(); ++i) { const auto k = static_cast<size_t>(extended[i].kind); ++all[k]; if (i < kExtendedBits) ++data[k]; } |
| 142 | const double expected = static_cast<double>(kExtendedBits) / extended.size(); |
| 143 | const char *names[3] = {"Cartesian", "ring", "ray"}; |
| 144 | for (size_t k = 0; k < 3; ++k) { |
| 145 | const double share = static_cast<double>(data[k]) / all[k]; |
| 146 | std::printf("Profile 3 %s sites carrying codeword bits: %.3f (overall %.3f)\n", names[k], share, expected); |
| 147 | CKEY_CHECK(std::abs(share - expected) < 0.03); |
| 148 | } |
| 149 | std::array<size_t, 3> text_all{}, text_data{}; |
| 150 | for (size_t i = 0; i < text.size(); ++i) { const size_t k = text[i].cart ? 0 : text[i].dial ? 1 : 2; ++text_all[k]; if (i < kTextCodewordBytes * 8) ++text_data[k]; } |
| 151 | const double text_expected = static_cast<double>(kTextCodewordBytes * 8) / text.size(); |
| 152 | for (size_t k = 0; k < 3; ++k) CKEY_CHECK(std::abs(static_cast<double>(text_data[k]) / text_all[k] - text_expected) < 0.06); |
| 153 | } |
| 154 | |
| 155 | // ---- Rendered safe area and dead space ---- |
| 156 | const Clearances p2{0.5, 0.5, 0.5, 3.4, 14.5, 40, 31, 1, text_geometry()}; |
| 157 | const Clearances p3{1.5, 1.5, 0.5, 7.0, 58.0, 16, 124, 4, extended_geometry()}; |
| 158 | const std::vector<std::pair<const char *, std::vector<uint8_t>>> p2_payloads = { |
| 159 | {"P2 random", pseudo_random(200, 1)}, {"P2 all-zero", std::vector<uint8_t>(200, 0)}, |
| 160 | {"P2 all-0xFF", std::vector<uint8_t>(200, 0xff)}, {"P2 short", {'C', 'K'}}}; |
| 161 | for (const auto &[label, payload] : p2_payloads) check_rendered_layout(render_text(payload), p2, 1.15, label); |
| 162 | const std::vector<std::pair<const char *, std::vector<uint8_t>>> p3_payloads = { |
| 163 | {"P3 random", pseudo_random(2048, 2)}, {"P3 all-zero", std::vector<uint8_t>(2048, 0)}, |
| 164 | {"P3 all-0xFF", std::vector<uint8_t>(2048, 0xff)}, {"P3 short", {'C', 'K'}}}; |
| 165 | for (const auto &[label, payload] : p3_payloads) check_rendered_layout(render_extended(payload), p3, 1.4, label); |
| 166 | |
| 167 | // ---- Known-answer vectors for the optical layout (integer-only, so |
| 168 | // platform independent): FNV-1a-64 over every site in placement order of |
| 169 | // its 32-bit identity (little-endian) followed by its printed value. The |
| 170 | // payload is the 18 ASCII bytes "CKey renderer test". Values are also |
| 171 | // stated in the format specifications. ---- |
| 172 | { |
| 173 | const std::string text_payload = "CKey renderer test"; |
| 174 | const std::vector<uint8_t> bytes(text_payload.begin(), text_payload.end()); |
| 175 | ckey_text_symbol ts{}; ckey_extended_symbol es{}; ckey_diagnostics d{}; |
| 176 | CKEY_CHECK(ckey_encode_text(bytes.data(), bytes.size(), &ts, &d) == CKEY_OK); |
| 177 | CKEY_CHECK(ckey_encode_extended(bytes.data(), bytes.size(), &es, &d) == CKEY_OK); |
| 178 | const auto fnv = [](uint64_t h, uint8_t b) { return (h ^ b) * 0x100000001b3ull; }; |
| 179 | uint64_t text_hash = 0xcbf29ce484222325ull, extended_hash = 0xcbf29ce484222325ull; |
| 180 | for (size_t i = 0; i < text.size(); ++i) { |
| 181 | for (int k = 0; k < 4; ++k) text_hash = fnv(text_hash, static_cast<uint8_t>(text[i].id >> (8 * k))); |
| 182 | text_hash = fnv(text_hash, text_site_value(&ts, i) ? 1 : 0); |
| 183 | } |
| 184 | for (size_t i = 0; i < extended.size(); ++i) { |
| 185 | for (int k = 0; k < 4; ++k) extended_hash = fnv(extended_hash, static_cast<uint8_t>(extended[i].id >> (8 * k))); |
| 186 | extended_hash = fnv(extended_hash, extended_site_value(&es, i) ? 1 : 0); |
| 187 | } |
| 188 | std::printf("Layout KAT: Profile 2 %016llx, Profile 3 %016llx; first P2 site ids %08x %08x %08x\n", |
| 189 | static_cast<unsigned long long>(text_hash), static_cast<unsigned long long>(extended_hash), text[0].id, text[1].id, text[2].id); |
| 190 | CKEY_CHECK(text_hash == kTextLayoutKat && extended_hash == kExtendedLayoutKat); |
| 191 | } |
| 192 | |
| 193 | // ---- Ring runs over half a turn need the SVG large-arc flag ---- |
| 194 | { |
| 195 | std::ostringstream o; o << std::fixed << std::setprecision(4); |
| 196 | o << "<svg viewBox=\"0 0 29 29\"><g fill=\"#000000\" stroke=\"#000000\">"; |
| 197 | svg_ring_run(o, 14.5, 5.0, 0.5, 100.5, 144.0, 0.1, "0.2000"); // 250 degrees clockwise from 12:00 |
| 198 | o << "</g></svg>"; |
| 199 | const auto svg = svgtest::parse(o.str()); |
| 200 | CKEY_CHECK(svg.prims.size() == 1); |
| 201 | const auto on_ring = [&](double degrees) { |
| 202 | const double a = degrees * kTau / 360.0; |
| 203 | return svgtest::prim_hit(svg.prims[0], 14.5 + 5.0 * std::sin(a), 14.5 - 5.0 * std::cos(a)); |
| 204 | }; |
| 205 | CKEY_CHECK(on_ring(10) && on_ring(125) && on_ring(240)); |
| 206 | CKEY_CHECK(!on_ring(300) && !on_ring(330)); |
| 207 | } |
| 208 | |
| 209 | // ---- A corrupt Profile 2 length field is an integrity failure ---- |
| 210 | { |
| 211 | std::vector<uint8_t> cw(kTextCodewordBytes, 0); |
| 212 | cw[0] = 0xc7; cw[1] = 0x4b; cw[2] = CKEY_TEXT_FORMAT_VERSION; cw[3] = 2; cw[4] = 0x01; cw[5] = 0x00; // 256 > 200 |
| 213 | rs_encode_generic(cw, kTextDataBytes, kTextParityBytes); |
| 214 | std::array<uint8_t, 256> out{}; size_t len = 0; ckey_diagnostics d{}; |
| 215 | CKEY_CHECK(ckey_decode_text(cw.data(), cw.size(), out.data(), out.size(), &len, &d) == CKEY_INTEGRITY_FAILURE); |
| 216 | } |
| 217 | std::printf("CKey layout tests passed\n"); |
| 218 | } |