code wiki / (root) / nx_symmetry_group.nx

nx_symmetry_group.nx source

↩ module page · 311 lines · 10533 B

1// nx_symmetry_group.nx -- multi-axis symmetry detector. 2// 3// Generalizes nx_compose_h_symmetry + nx_compose_v_symmetry from 4// horizontal/vertical bilateral to: 5// * diagonal mirror (axis x = y) 6// * anti-diagonal mirror (axis x = -y) 7// * 180-degree rotational 8// * 90-degree rotational (square only) 9// * translational periodic 10// 11// Returns per-axis Q10 scores + the sealed-enum kind of the dominant 12// symmetry + a composite verdict. Multiple axes can fire concurrently 13// (a square checkerboard has H + V + DIAG + 4_FOLD + TRANSLATIONAL all 14// near max); the report carries them all so downstream tooling sees 15// the full symmetry group, not just the maximum. 16// 17// Cross-modal kernel: the algorithms here operate on any indexed 18// array. Today's signature takes *Image, but the substrate will 19// later generalize to spectrograms, attention heatmaps, terrain 20// arrays, palindrome detection over token streams, etc. 21// 22// Sealed-enum SymmetryKind (gap-2 mitigation via constants): 23// NX_SYM_NONE 0 24// NX_SYM_H_BILATERAL 1 25// NX_SYM_V_BILATERAL 2 26// NX_SYM_DIAGONAL 3 27// NX_SYM_ANTIDIAGONAL 4 28// NX_SYM_2_FOLD_ROT 5 29// NX_SYM_4_FOLD_ROT 6 30// NX_SYM_TRANSLATIONAL 7 31// 32// genealogy_id: weyl_1952_symmetry + tyler_1995_visual_symmetry + 33// wagemans_1995_psychophysics_symmetry 34// lineage_id: multi_axis_symmetry_group_q10 35 36// nx_safety_envelope: 37// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 38// sil_target: SIL1 39// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 40// verdict: NOT_YET_EVALUATED 41 42import "nx_syscalls.nx" 43import "nx_tier.nx" 44import "nx_image.nx" 45import "nx_compose.nx" 46 47const NX_SYM_NONE: nx_int = 0 48const NX_SYM_H_BILATERAL: nx_int = 1 49const NX_SYM_V_BILATERAL: nx_int = 2 50const NX_SYM_DIAGONAL: nx_int = 3 51const NX_SYM_ANTIDIAGONAL: nx_int = 4 52const NX_SYM_2_FOLD_ROT: nx_int = 5 53const NX_SYM_4_FOLD_ROT: nx_int = 6 54const NX_SYM_TRANSLATIONAL: nx_int = 7 55const NX_SYM_N_KINDS: nx_int = 8 56 57const NX_SYM_Q: nx_int = 1024 58// Symmetry score >= this threshold counts as "present" for that axis. 59const NX_SYM_PRESENT_THR: nx_int = 717 // ~70% Q10 60 61struct SymmetryReport { 62 h_bilateral_q10: nx_int, 63 v_bilateral_q10: nx_int, 64 diagonal_q10: nx_int, 65 antidiagonal_q10: nx_int, 66 rot_180_q10: nx_int, 67 rot_90_q10: nx_int, 68 translational_q10: nx_int, 69 translation_period: nx_int, // best matching period, 0 if none 70 dominant_kind: nx_int, // NX_SYM_* constant 71 composite_q10: nx_int, // max across all axes 72 n_axes_present: nx_int, // how many >= NX_SYM_PRESENT_THR 73} 74 75// ===== Diagonal mirror (along x = y) =================================== 76// 77// Compares pixel(x, y) to pixel(y, x). Square images only; for non- 78// square the function returns 0 (axis not measurable). 79 80func nx_sym_diagonal(gray: *Image) -> nx_int { 81 let w: nx_int = gray.width 82 let h: nx_int = gray.height 83 if w != h { return 0 } 84 var diff_sum: nx_int = 0 85 var total: nx_int = 0 86 var y: nx_int = 0 87 while y < h { 88 var x: nx_int = 0 89 while x < y { // only below diagonal 90 let av: nx_int = nx_image_get(gray, x, y, 0) 91 let bv: nx_int = nx_image_get(gray, y, x, 0) 92 var d: nx_int = av - bv 93 if d < 0 { d = -d } 94 diff_sum = diff_sum + d 95 total = total + av + bv 96 x = x + 1 97 } 98 y = y + 1 99 } 100 if total == 0 { return NX_SYM_Q } 101 let asym: nx_int = (diff_sum * NX_SYM_Q) / total 102 if asym >= NX_SYM_Q { return 0 } 103 return NX_SYM_Q - asym 104} 105 106// ===== Anti-diagonal mirror (along x = -y) ============================= 107 108func nx_sym_antidiagonal(gray: *Image) -> nx_int { 109 let w: nx_int = gray.width 110 let h: nx_int = gray.height 111 if w != h { return 0 } 112 var diff_sum: nx_int = 0 113 var total: nx_int = 0 114 var y: nx_int = 0 115 while y < h { 116 var x: nx_int = 0 117 while x < w - 1 - y { 118 let av: nx_int = nx_image_get(gray, x, y, 0) 119 let bv: nx_int = nx_image_get(gray, w - 1 - y, h - 1 - x, 0) 120 var d: nx_int = av - bv 121 if d < 0 { d = -d } 122 diff_sum = diff_sum + d 123 total = total + av + bv 124 x = x + 1 125 } 126 y = y + 1 127 } 128 if total == 0 { return NX_SYM_Q } 129 let asym: nx_int = (diff_sum * NX_SYM_Q) / total 130 if asym >= NX_SYM_Q { return 0 } 131 return NX_SYM_Q - asym 132} 133 134// ===== 180-degree rotational ========================================== 135// 136// Equivalent to (H * V) symmetry; an image symmetric under 180 rotation 137// is symmetric under (x,y) -> (W-1-x, H-1-y). 138 139func nx_sym_rot_180(gray: *Image) -> nx_int { 140 let w: nx_int = gray.width 141 let h: nx_int = gray.height 142 var diff_sum: nx_int = 0 143 var total: nx_int = 0 144 var y: nx_int = 0 145 while y < h / 2 { 146 var x: nx_int = 0 147 while x < w { 148 let av: nx_int = nx_image_get(gray, x, y, 0) 149 let bv: nx_int = nx_image_get(gray, w - 1 - x, h - 1 - y, 0) 150 var d: nx_int = av - bv 151 if d < 0 { d = -d } 152 diff_sum = diff_sum + d 153 total = total + av + bv 154 x = x + 1 155 } 156 y = y + 1 157 } 158 if total == 0 { return NX_SYM_Q } 159 let asym: nx_int = (diff_sum * NX_SYM_Q) / total 160 if asym >= NX_SYM_Q { return 0 } 161 return NX_SYM_Q - asym 162} 163 164// ===== 90-degree rotational (square only) ============================= 165// 166// Maps (x, y) -> (y, W-1-x). Requires square image. 167 168func nx_sym_rot_90(gray: *Image) -> nx_int { 169 let w: nx_int = gray.width 170 let h: nx_int = gray.height 171 if w != h { return 0 } 172 var diff_sum: nx_int = 0 173 var total: nx_int = 0 174 var y: nx_int = 0 175 while y < h { 176 var x: nx_int = 0 177 while x < w { 178 let av: nx_int = nx_image_get(gray, x, y, 0) 179 let bv: nx_int = nx_image_get(gray, y, w - 1 - x, 0) 180 var d: nx_int = av - bv 181 if d < 0 { d = -d } 182 diff_sum = diff_sum + d 183 total = total + av + bv 184 x = x + 1 185 } 186 y = y + 1 187 } 188 if total == 0 { return NX_SYM_Q } 189 let asym: nx_int = (diff_sum * NX_SYM_Q) / total 190 if asym >= NX_SYM_Q { return 0 } 191 return NX_SYM_Q - asym 192} 193 194// ===== Translational periodic detection =============================== 195// 196// For periods p = 2, 4, 8, ..., w/2: compare each pixel to its 197// period-shifted counterpart. Returns the BEST score across periods 198// and stores the matching period in *out_period (0 if none above 199// threshold). Cross-modal: same primitive detects musical-bar 200// repetition over a time-series, prose-cadence over token streams. 201 202func nx_sym_translational(gray: *Image, out_period: *i64) -> nx_int { 203 let w: nx_int = gray.width 204 let h: nx_int = gray.height 205 var best_score: nx_int = 0 206 var best_period: nx_int = 0 207 var p: nx_int = 2 208 while p <= w / 2 { 209 var diff_sum: nx_int = 0 210 var total: nx_int = 0 211 var y: nx_int = 0 212 while y < h { 213 var x: nx_int = 0 214 while x < w - p { 215 let av: nx_int = nx_image_get(gray, x, y, 0) 216 let bv: nx_int = nx_image_get(gray, x + p, y, 0) 217 var d: nx_int = av - bv 218 if d < 0 { d = -d } 219 diff_sum = diff_sum + d 220 total = total + av + bv 221 x = x + 1 222 } 223 y = y + 1 224 } 225 var score: nx_int = 0 226 if total == 0 { 227 score = NX_SYM_Q 228 } else { 229 let asym: nx_int = (diff_sum * NX_SYM_Q) / total 230 if asym < NX_SYM_Q { score = NX_SYM_Q - asym } 231 } 232 if score > best_score { 233 best_score = score 234 best_period = p 235 } 236 p = p * 2 237 } 238 out_period[0] = best_period 239 return best_score 240} 241 242// ===== Composite ===================================================== 243// 244// Runs all axes, picks the dominant kind, counts how many are present. 245 246func nx_symmetry_compute(gray: *Image, report: *SymmetryReport) -> nx_int { 247 report.h_bilateral_q10 = nx_compose_h_symmetry(gray) 248 report.v_bilateral_q10 = nx_compose_v_symmetry(gray) 249 report.diagonal_q10 = nx_sym_diagonal(gray) 250 report.antidiagonal_q10 = nx_sym_antidiagonal(gray) 251 report.rot_180_q10 = nx_sym_rot_180(gray) 252 report.rot_90_q10 = nx_sym_rot_90(gray) 253 let period_buf: *i64 = (sys_mmap(NX_SIZEOF_NX_INT)) as *i64 254 report.translational_q10 = nx_sym_translational(gray, period_buf) 255 report.translation_period = period_buf[0] 256 257 // Find dominant kind: highest scoring axis. 258 var best: nx_int = 0 259 var kind: nx_int = NX_SYM_NONE 260 261 if report.h_bilateral_q10 > best { 262 best = report.h_bilateral_q10 263 kind = NX_SYM_H_BILATERAL 264 } 265 if report.v_bilateral_q10 > best { 266 best = report.v_bilateral_q10 267 kind = NX_SYM_V_BILATERAL 268 } 269 if report.diagonal_q10 > best { 270 best = report.diagonal_q10 271 kind = NX_SYM_DIAGONAL 272 } 273 if report.antidiagonal_q10 > best { 274 best = report.antidiagonal_q10 275 kind = NX_SYM_ANTIDIAGONAL 276 } 277 if report.rot_180_q10 > best { 278 best = report.rot_180_q10 279 kind = NX_SYM_2_FOLD_ROT 280 } 281 if report.rot_90_q10 > best { 282 best = report.rot_90_q10 283 kind = NX_SYM_4_FOLD_ROT 284 } 285 if report.translational_q10 > best { 286 best = report.translational_q10 287 kind = NX_SYM_TRANSLATIONAL 288 } 289 report.composite_q10 = best 290 report.dominant_kind = kind 291 292 // Count axes meeting the present-threshold. 293 var n: nx_int = 0 294 if report.h_bilateral_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 295 if report.v_bilateral_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 296 if report.diagonal_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 297 if report.antidiagonal_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 298 if report.rot_180_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 299 if report.rot_90_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 300 if report.translational_q10 >= NX_SYM_PRESENT_THR { n = n + 1 } 301 report.n_axes_present = n 302 303 return 0 304} 305 306// Sealed-enum validity check (gap-2 mitigation pattern). 307func nx_sym_kind_is_valid(kind: nx_int) -> nx_int { 308 if kind < 0 { return 0 } 309 if kind >= NX_SYM_N_KINDS { return 0 } 310 return 1 311}