Skip to content

Commit b184c5c

Browse files
committed
re(RE-14.3): ArcWall record wire format decoded — 28 walls on Einhoven
Hex-dumped all 32 filtered ArcWall (tag 0x0191) records on Einhoven Partitions/5. Wire format is fully deterministic. Standard ArcWall record (292 B singleton, 568 B pair): +0x00 u16 tag = 0x0191 +0x02 u16 filter_pad = 0x0000 +0x04 u32 fixed_header_0 = 0x00088004 (class-family marker) +0x08 u32 count_version = 1 for standard, 3 for compound +0x0c u32 type_code = 0x00000003 +0x10 u16 variant = 0x07fa standard, 0x0821 compound +0x12 f64 x 6 = 6 doubles of wall geometry +0x42 f64 x 6 = 6 doubles duplicate (diff coord system?) +0x72 u8 trailer = 0x03 Record-count breakdown of the 32 occurrences: - 1 index record (#0) : list of u32 IDs (manifest) - 3 metadata records (#1-#3): contain schema constants 0x576, 0x1d94 (same constants RE-14.1 found in HostObjAttr — cross-corpus coherence) - 2 compound walls (#12, #13): variant 0x0821, embedded openings - ~26 standard walls : variant 0x07fa, clean geometry Net: 28 architectural wall elements decodable on Einhoven directly. New hypotheses: H14 (0.95) — records are self-describing by tag + variant, fixed body per variant H15 (0.9) — constants 0x88004, 0x576, 0x1d94 are schema-family IDs stable across record types in the HostObj/ArcWall family H16 (0.75) — coord pairs = two 3D points (line-segment endpoints of wall centerline) Decisions: D19 — Build elements::decoders::arc_wall as first concrete decoder D20 — Build walker::iter_arc_walls() as RE-15 stand-in D21 — Wire ArcWallRecord to IFCWALL in exporter D22 — DEC-05 (IfcWall count > 0) is now unblocked Synthesis: reports/element-framing/RE-14.3-synthesis.md with full byte-level record layout, 10+ sample decodings, decoder sketch in Rust, hypothesis set, open questions (coord semantics, variant enum, wall-type lookup path). Next concrete step: implement arc_wall.rs decoder + test on 28 records + emit 28 IFCWALL entities via exporter.
1 parent 80d8e3f commit b184c5c

2 files changed

Lines changed: 430 additions & 0 deletions

File tree

examples/probe_arcwall_records.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! RE-14.3 — ArcWall record layout RE on Einhoven Partitions/5.
2+
//!
3+
//! Per D17 of `RE-14.2-synthesis.md`, ArcWall (tag 0x0191) has 32
4+
//! occurrences in Einhoven Partitions/5, all passing the record-prefix
5+
//! filter. This is the cleanest wall-like signal in the corpus —
6+
//! small sample, 100% real, no text-artifact noise.
7+
//!
8+
//! This probe:
9+
//! 1. Finds all 32 occurrences of tag 0x0191 with buf[+2..+4] == 0x0000.
10+
//! 2. For each, hex-dumps 128 B starting from the tag byte.
11+
//! 3. Computes forward-distance-to-next-real-ArcWall.
12+
//! 4. Per-column byte-value histograms at +2..+64 to find fixed
13+
//! fields vs variable fields vs length prefixes.
14+
//! 5. Scans each record for embedded u32 values that might be IDs
15+
//! (references to ElemTable entries, owner IDs, parent refs).
16+
//!
17+
//! Expected outcome: enough evidence of the record envelope to write
18+
//! a concrete ArcWall decoder and wire it into the IFC exporter.
19+
20+
use rvt::{RevitFile, compression, streams};
21+
use std::collections::BTreeMap;
22+
23+
fn main() {
24+
let project_dir = std::env::var("RVT_PROJECT_CORPUS_DIR")
25+
.unwrap_or_else(|_| "/private/tmp/rvt-corpus-probe/magnetar/Revit".into());
26+
let target_tag: u16 = 0x0191; // ArcWall on Einhoven 2023
27+
let file = "Revit_IFC5_Einhoven.rvt";
28+
let partition = "Partitions/5";
29+
let dump_len = 128;
30+
31+
let path = format!("{project_dir}/{file}");
32+
let mut rf = RevitFile::open(&path).unwrap();
33+
let _ = streams::FORMATS_LATEST;
34+
let raw = rf.read_stream(partition).unwrap();
35+
let chunks = compression::inflate_all_chunks(&raw);
36+
let concat: Vec<u8> = chunks.into_iter().flatten().collect();
37+
38+
// Find occurrences of 0x0191 passing the record-prefix filter.
39+
let mut occurrences: Vec<usize> = Vec::new();
40+
for i in 0..concat.len().saturating_sub(3) {
41+
let v = u16::from_le_bytes([concat[i], concat[i + 1]]);
42+
if v != target_tag {
43+
continue;
44+
}
45+
if concat[i + 2] == 0x00 && concat[i + 3] == 0x00 {
46+
occurrences.push(i);
47+
}
48+
}
49+
50+
println!("=== {file} {partition} — ArcWall (0x{target_tag:04x}) records ===");
51+
println!(
52+
" Buffer: {} bytes, {} clean ArcWall occurrences",
53+
concat.len(),
54+
occurrences.len(),
55+
);
56+
57+
// Distance-to-next histogram.
58+
println!("\n Distance-to-next-ArcWall:");
59+
let mut deltas: Vec<usize> = Vec::new();
60+
for w in occurrences.windows(2) {
61+
deltas.push(w[1] - w[0]);
62+
}
63+
if !deltas.is_empty() {
64+
let min = *deltas.iter().min().unwrap();
65+
let max = *deltas.iter().max().unwrap();
66+
let mean = deltas.iter().sum::<usize>() as f64 / deltas.len() as f64;
67+
let mut sorted = deltas.clone();
68+
sorted.sort_unstable();
69+
let median = sorted[sorted.len() / 2];
70+
println!(
71+
" count={}, min={}, max={}, mean={:.0}, median={}",
72+
deltas.len(),
73+
min,
74+
max,
75+
mean,
76+
median,
77+
);
78+
// Show all deltas as a sequence.
79+
let delta_str = deltas
80+
.iter()
81+
.map(|d| d.to_string())
82+
.collect::<Vec<_>>()
83+
.join(", ");
84+
println!(" deltas: [{}]", delta_str);
85+
}
86+
87+
// Per-column byte histograms at +2..+64.
88+
println!("\n Column-wise byte histogram (+2..+32 after tag start):");
89+
println!(
90+
" {:>4} {:>10} {:>10} {:>10} {:>6}",
91+
"col", "top1", "top2", "top3", "uniq"
92+
);
93+
for c in 2..32 {
94+
let mut hist: BTreeMap<u8, usize> = BTreeMap::new();
95+
for &off in &occurrences {
96+
if off + c < concat.len() {
97+
*hist.entry(concat[off + c]).or_insert(0) += 1;
98+
}
99+
}
100+
let mut sorted: Vec<(u8, usize)> = hist.into_iter().collect();
101+
sorted.sort_by_key(|(_, v)| std::cmp::Reverse(*v));
102+
let pick = |i: usize| -> String {
103+
sorted
104+
.get(i)
105+
.map(|(v, c)| format!("0x{v:02x} ({})", c))
106+
.unwrap_or_else(|| "—".to_string())
107+
};
108+
println!(
109+
" {:>4} {:>10} {:>10} {:>10} {:>6}",
110+
c - 2,
111+
pick(0),
112+
pick(1),
113+
pick(2),
114+
sorted.len()
115+
);
116+
}
117+
118+
// Hex-dump all 32 occurrences.
119+
println!("\n Hex-dump of all records (128 B each from tag start):");
120+
for (i, &off) in occurrences.iter().enumerate() {
121+
let end = (off + dump_len).min(concat.len());
122+
let bytes = &concat[off..end];
123+
println!("\n #{i:>2} @ offset {off:>6} (delta_next={}):",
124+
if i + 1 < occurrences.len() {
125+
(occurrences[i + 1] - off).to_string()
126+
} else {
127+
"—".to_string()
128+
}
129+
);
130+
// 16 bytes per line, with ASCII sidebar.
131+
for (row, chunk) in bytes.chunks(16).enumerate() {
132+
let hex_part: String = chunk
133+
.iter()
134+
.map(|b| format!("{b:02x}"))
135+
.collect::<Vec<_>>()
136+
.join(" ");
137+
let ascii_part: String = chunk
138+
.iter()
139+
.map(|b| if b.is_ascii_graphic() || *b == b' ' { *b as char } else { '.' })
140+
.collect();
141+
println!(" +{:03x}: {:<48} {}", row * 16, hex_part, ascii_part);
142+
}
143+
}
144+
145+
// Look for any u32 values that repeat across records — these are
146+
// likely structural constants (class ID, schema version, etc).
147+
println!("\n u32 constants appearing in >=4 records at any position within first 128 B:");
148+
let mut u32_votes: BTreeMap<u32, usize> = BTreeMap::new();
149+
for &off in &occurrences {
150+
let slice_end = (off + dump_len).min(concat.len());
151+
let slice = &concat[off..slice_end];
152+
if slice.len() < 4 {
153+
continue;
154+
}
155+
// Unique u32 values in this record (avoid double-counting within same record).
156+
let mut seen: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
157+
for w in 0..slice.len().saturating_sub(3) {
158+
let v = u32::from_le_bytes([slice[w], slice[w + 1], slice[w + 2], slice[w + 3]]);
159+
if v == 0 || v == u32::MAX {
160+
continue;
161+
}
162+
seen.insert(v);
163+
}
164+
for v in seen {
165+
*u32_votes.entry(v).or_insert(0) += 1;
166+
}
167+
}
168+
let mut top_u32: Vec<(u32, usize)> = u32_votes.into_iter().filter(|(_, v)| *v >= 4).collect();
169+
top_u32.sort_by_key(|(_, v)| std::cmp::Reverse(*v));
170+
println!(" {:>12} {:>12} {:>10}", "u32 (hex)", "decimal", "records");
171+
for (v, count) in top_u32.iter().take(20) {
172+
println!(" 0x{v:08x} {v:>12} {count:>10}");
173+
}
174+
}

0 commit comments

Comments
 (0)