Skip to content

Latest commit

 

History

History
311 lines (272 loc) · 38.1 KB

CHANGELOG.md

File metadata and controls

311 lines (272 loc) · 38.1 KB

Changelog

All notable changes to this project are documented here, in Keep a Changelog style. main is reserved for releases; develop is the working branch.

[Unreleased]

Changed

  • Moved the eleven bem_* root modules into a new bem/ package (#1179, chunk 1): import them as bem.bem_export, bem.bem_geometry, and so on. The public API surface pin now points at the new names.

Added

  • Benchmark ground truth is now independent of matchline's IFC reader (#1165): scripts/bench_truth.py reads room areas, exterior wall length and windows/doors straight from each benchmark IFC with ifcopenshell and freezes them as truth.json (sha256-pinned to the IFC). The scoreboard's area error is scored against that truth by IfcSpace GlobalId, and the hard-coded wall_len_err = 0.0 is replaced by a measured exterior wall length error (blank when the IFC states no IsExternal wall).
  • Five IFC-Bench buildings in the real-building benchmark config (#1165): ac20 and fzk-house (KIT, CC BY 4.0), digital-hub with its ventilation model (RWTH E3D, MIT), smiley-west (KIT, CC BY 4.0), and wbdg-office (license under review). Each is in license_ledger.json; data stays outside the repo.

[0.2.1] - 2026-10-19

Added

  • Detector: wall-class auxiliary head (#1113) with scratch-provider checkpoint loading (#1152). Classification-only (partition vs structural), raster +1 shift, old datasets train unchanged.
  • Detector: resumable train state for crash recovery (#1116), with a --train-env-extra dispatch input for fault localization, eval-only mode skipping train-metrics upload (#1119), and a torch-free resume test path.
  • Detector: AP50 metric for scratch checkpoints alongside F1 (#1119).
  • Schedule evaluation: grouped-vs-flat schedule-row scorer (#1112) and noise-conditioned schedule evaluation (KR/EM/VF1) (#1114).
  • Research docs: ranked shortlist for training/research features with a first-experiment spec (#1095); 2026-10 arXiv sweep mapped to pipeline slices (#1094).

Changed

  • CI: the dependency security audit drops the project itself before scanning, so pre-release version bumps no longer trip --strict on an unpublished version (pip-audit --skip-editable still reports editable distributions as errors).

  • Test coverage: property pins for the three deliberate point-in-polygon contracts (#1129), direct unit tests for opening identity, opening attach, and the PRM table (#1128), golden coverage beyond the validate reports (#1127), telemetry + feedback round trip through run_review (#1126), degenerate-input battery (#1124), and per-subcommand CLI contract tests (#1123).

  • CI: pip-audit of the runtime dependency set on every PR (#1139), coverage measurement of the shipped surface (#1137), mypy baseline on a scoped module set (#1138), ruff pinned to 0.17.1 (#1141), ci.yml python held at 3.12 while openstudio segfaults under 3.14+ (#1157), GitHub Actions bumps via renovate (#1155), weekly cleanup-sweep workflow (#1121), and repo hygiene (tracked junk dropped, session artifacts moved under docs/history/, runtime feedback log untracked) (#1108-#1110).

Fixed

  • Detector: merge tile-boundary straddle duplicates in SAHI NMS (#1125).
  • Release: skip publish when the version is already on PyPI and tag releases on main (#1102), parse the version from the wheel filename correctly (#1105), and empty-commit check scanning (#1104).

[0.2.0] - 2026-10-16

Added

  • hvac_trace.trace_symbols_to_terminals: multi-source duct skeleton trace that assigns each legend-referenced terminal to the zone its ductwork feeds (#744). Seeds at terminal symbols, walks the skeleton with a shared ASSOC_PX reach, and labels runs contested when two terminals claim the same skeleton, and leaves a run no tagged terminal seeds unassigned; off-duct terminals stay unassigned. real_set.py records duct / duct_ambiguous stats and notes the zone each terminal feeds ("it sits on that zone's ductwork (skeleton trace, ...)"), so ambiguous traces surface as review instead of guesses. Five new trace tests plus a shared-room duct test in the real-set fixture.

Added

  • detector/train_scratch.py: from-scratch YOLO-style single-shot detector trainer for the synth track (#743). Grayscale letterbox input, stride-8 grid, per-cell class + objectness + CIoU box losses, cell-relative box targets, class-agnostic NMS decode, IoU-matched per-class evaluation, deterministic seeded training. License-clean: custom PyTorch only, no ultralytics weights or code paths in shipped artifacts. Outputs (ckpt.pt, metrics.json) are written to a caller-set run directory, never the repo. 6 tests in detector/tests/test_train_scratch.py (CI runs lint-only on detector code by policy; suite verified locally).

Added

  • detector/convert_synth.py: synth gt.json -> Ultralytics YOLO converter (#743): maps sheet symbols through the unified taxonomy (AEC_MAP), clips and normalizes boxes, deterministic seeded train/val split, data.yaml output.

  • scripts/benchmark.py: real-building benchmark harness (#748): per-building IFC-path metrics, HVAC-terminal check, cross-frontend comparison and OpenStudio gbXML import check from an external config; scoreboard markdown, data outside the repo, DATASETS.md records candidates and licenses.

  • Synthetic second-style mech sheets (#744): generate_mech_sheet(..., style=1) draws every terminal in the class's other drafting convention (alt glyphs added for VAV, AHU and thermostat; diffuser and grille had theirs) and legend=True draws an HVAC symbol legend in a band below the plan, with gt["legend"] holding sheet-dict fragments for hvac_legend.read_legend. End-to-end tests pin: the drawn legend reads back mapped, templates are cut at the drawn size, legend-template hits are true terminals only, and the ducted-terminal NCC gap stays pinned as the open limitation.

  • Vestibule detection (#747): a room is marked is_vestibule when it meets Alex's rule (2026-10-11): it has an exterior door, its floor area is at or under 15 sq m (Fo's default, flagged for Alex), and it connects to a lobby or main entrance (a neighboring room named LOBBY / RECEPTION / ENTRY / FOYER / WAITING, or the door's far side). Detection never reclassifies a space; the Section 3.2 category still comes from the HVAC capacity rules alone. Every call and the default threshold go to review.

  • matchline demo (#753): sample project end to end in one command, printing where the trust report, review report and exports landed and opening the trust report on an interactive terminal. matchline run no longer crashes on the seed path (cli Namespace carried no image attribute).

  • Review: an edit on an IFC opening-attachment item now changes the model (#799): the opening moves into the room the reviewer types (space id or unique room number). A skylight keeps its plan centre, tilt and azimuth and the room's daylight is recomputed; a wall opening keeps its sill and gets a host interval centred on its plan centre; an already-attached opening may move between rooms on the same level and a cross-level value is refused. An edit on a BIM element item still records only, and an item whose opening is no longer in the model degrades to record-only. Every edit value is checked before anything changes

  • Review: an edit on a fixture, diffuser or sensor assignment item now changes the model like an opening edit (#799): the device moves to the room the reviewer types (space id or unique room number), a fixture the linker left unassigned is created there from the data the lighting linker put in the item's target, the room's lighting rollups follow, and a diffuser or sensor edit mirrors the zone membership the mechanical linker maintains. Every edit value is checked before anything changes, revert/confirm/reject restore the automatic value, and items from models written before #799 stay record-only.

  • Model: floor/ceiling boundaries between levels are recorded on BuildingModel.semi_exterior alongside the walls (#900). A conditioned space under or over a semiheated/unconditioned space on an adjacent level is semi-exterior envelope (90.1-2019 Section 3.2 / Figure 5.5.2); an unsettled side goes to review. Floor records take no construction, so exports are unchanged.

  • BEM export: scheduled air handlers are written to gbXML as air loops. Each AHU/RTU on the mechanical schedules becomes an AirLoop named by its tag, with a supply fan carrying its scheduled design airflow (MaximumFlow, L/s) and cooling and heating coils carrying its scheduled capacities (Btu/h), the schedule sources in its description; systemType is VariableAirVolume when boxes hang off it. Each HVAC zone records the air handler feeding its box from the box-to-unit links on the schedules (Zone.air_loop, BuildingModel.air_loops), and each gbXML thermal Zone references its air handler (AirLoopId) when the HVAC zones serving it all name the same one; a thermal zone served by HVAC zones with no known air handler, by more than one, or by none gets no reference and an air_loop review item. Box airflow summing above the supply fan airflow is reported as diversity, never as a problem (air_loops.py, Stage 4 and Stage 6) (#746).

  • BEM export: semi-exterior walls take the Table 5.5 Semiheated construction. ASHRAE 90.1-2019 5.5.2 has the semiexterior envelope meet the semiheated requirements of Tables 5.5-0 to 5.5-8, so with a climate zone every settled BuildingModel.semi_exterior boundary gets t55-semiext-wall (Semiheated ExteriorWall steel-framed row, construction_default provenance citing 5.5.2), and the gbXML interior wall between that pair references it as a layered construction instead of the partition; review boundaries keep the partition (construction_library._default_semi_exterior, constructions.semi_exterior_walls, BEMModel.interior_wall_constructions) (#747).

  • BEM export: settled semi-exterior floor/ceiling boundaries (#900) now take the Table 5.5 Semiheated construction too. ASHRAE 90.1-2019 5.5.2 has the semiexterior envelope meet the semiheated requirements of Tables 5.5-0 to 5.5-8, so with a climate zone every settled BuildingModel.semi_exterior floor/ceiling record gets t55-semiext-floor (Semiheated ExteriorFloor steel-framed row, construction_default provenance citing 5.5.2), and the multi-storey gbXML InteriorFloor between that pair references it as a layered construction instead of the no-U partition; review boundaries and single-storey exports are unchanged (construction_library._default_semi_exterior_floor, constructions.semi_exterior_floors, BEMModel.interior_floor_constructions) (#900).

  • BEM export: scheduled zone airflow now reaches gbXML as FlowPerArea (m3/s per m2) on each thermal Zone, the summed max airflow of the HVAC zones serving it over its floor area, written only where every space in the zone is served by HVAC zones that serve nothing outside it; any other thermal zone gets no value and a zone_airflow review item naming why. Split-room pieces count as their parent room; with no Appendix G thermal zones the single default zone is used. The min airflow stays in the JSON report (zone_airflow.py, Stage 6) (#746).

Fixed

  • Door detection: a double door (two leaves hung from opposite jambs, their tips meeting at the middle) is now reported as one door from hinge to hinge, as wide as both leaves, instead of two half-width doors (door_detect._pairs). In a thin partition the pair is taken when a wall opening about as wide as both leaves lies across it; a single leaf still needs an opening about as wide as its own swing. Synthetic double door in a 2 px and a 20 px wall: one door in all 8 orientations (the thin one was missed before, the thick one was two doors); without an opening the thin pair is still not taken, and two leaves whose far jambs do not meet stay two doors. So a double door in a thin partition has an opening to stand on, plan_walls thin gaps now run up to a double door (THIN_GAP_MAX_M 1.35 to 2.7 m); walls, openings and rooms are unchanged, since only the door reader uses thin gaps. The door reader also takes the wall reader's openings, not only its thin gaps, for a door in a wall thinner than its jamb floor: a 0.054 m partition is a wall to plan_walls (over WALL_T_MIN_M), so its door gap is an opening (detection_provider.opening_segments, Clinic validator plumbing) (#875).
  • Door detection: a door in a thin partition is now found when a neighbouring door's leaf, open along the partition's other face, sits just behind the hinge. The wall read behind the hinge took the first ink it met, which was that leaf, so the hinge wall was lost; when that read fails, door_detect._wall_at now also tries the ink nearest the hinge (_band_at(nearest=True)). Synthetic door with the neighbouring leaf went from missed in 8 of 8 orientations to found in all 8, and without a wall opening it is still not taken. Clinic doors: first floor unchanged at recall 0.9932, precision 1.0; second floor recall 0.9792 to 0.9896, precision 0.9897 to 0.9898 (#875).
  • Clinic validator: a door whose jamb sits on the face of a long cross wall now gets a swing as wide as the door. The wall-face test in glazing_line scored that cross wall's face, picked the opening's depth (0.12 m) as its width, and drew a blob the detector could not read; door_swing now uses the opening's long side when the wall-face line is narrower than the narrowest swing door (door_detect.MIN_WIDTH_M). Windows are unchanged. Clinic doors (plumbing only, door_detect unchanged): first floor recall 0.9865 to 0.9932, precision 1.0; second floor recall 0.9583 to 0.9792, precision 0.9895 to 0.9897 (#875).
  • HVAC detection: a grille or diffuser whose WiSARD label fails the tight confirm takes the NCC proposal's class when the proposal named the other terminal class and that class's tight template scores at least 0.76 (hvac_trace.RELABEL_CONFIRM_NCC, _confirm_label); a label that confirms is never changed. Clinic grilles (detector measured only): clean unchanged at 123/0 and 83/0 TP/FP; line weight 94 to 118 and 63 to 81 found; scale 76 to 96 and 62 to 69; --vary 1 11 to 18 (one more false grille) and 17 to 19; --vary 2 19 to 28 and 12 to 17. Diffusers unchanged. The 0.76 bar was read off Clinic; follow-up #887 (#745).
  • Door detection: a door whose far jamb is a wall that a crossing wall meets in a T right past the opening is now found; when the jamb is seen on the first row past the opening but the second row runs into the crossing wall, the jamb is read again further along (door_detect._canonical). Synthetic door went from missed in 8 of 8 orientations to found in all 8, and the same jamb with no gap is still not a door. Clinic doors (plumbing only): first floor 141 to 142 of 148, recall 0.959 to 0.966, precision unchanged at 1.0; second floor unchanged (#875).
  • Door detection: a door whose leaf is drawn just off a crossing wall's face (separable from it, so not read as a leaf on the face) is now found when its hinge wall runs on beyond that crossing wall; when the row behind the hinge runs along the crossing wall, the hinge wall is read on its far side (door_detect._canonical, _run_through). Synthetic door went from missed in 8 of 8 orientations to found in all 8, and the same leaf against an unbroken wall is still not a door. Clinic doors (plumbing only): first floor 140 to 141 of 148, recall 0.953 to 0.959, precision unchanged at 1.0; second floor unchanged (#875).
  • Door detection: a door whose far jamb is the hinge stub of a door hung in the crossing wall (that door's leaf lying just off the wall line across the opening, the two swings crossing) is now found; when the wall just past the jamb gives no clear opening, the jamb wall is read again further along (door_detect._canonical). Synthetic stub-jamb door went from missed to found in all 8 orientations, and a stub against an unbroken wall is still not a door. Clinic doors (plumbing only): first floor recall 0.946 to 0.953, second floor 0.917 to 0.927, precision unchanged at 1.0 and 0.989 (#875).
  • CI: the empty-commit check skips merge commits. git diff-tree lists no files for a merge, so every merge of develop into a PR branch failed the check; plain commits are still checked.
  • Plan walls: a window through an asymmetric double-leaf wall whose glass line falls on one leaf's cavity face is now found; the two walls that meet on the glass line and together fill the glazed band no longer count as a wall inside the band (plan_walls._split_by_glass_line). Synthetic 0.10/0.10/0.20 m leaf/cavity/leaf walls (and three like them) went from missed to found; Clinic walls, rooms and windows are unchanged (#877).
  • Plan walls: a short lined stretch (a lining a gap off the wall's inside face, closed at both ends by its return and a corner) is no longer called a window; a thinner parallel wall flush against a glazed band's face now rejects the candidate like one inside it (plan_walls._wall_inside_band). Synthetic 1.64 m stretch went from a false window to none; Clinic walls, rooms and windows are unchanged (#879).
  • Door detection: a door hung on one face line of a hollow jamb wall (two thin lines with a clear gap wider than two leaves) is now found; the hollow wall also offers its face lines as the hinge, tried with that wall alone, and the arc, leaf-reach and clear-opening checks are unchanged (door_detect._hinge_options, _hollow). Clinic doors (plumbing only): first floor 139 to 140 of 148, recall 0.946 to 0.953, precision 1.0; second floor unchanged (#875).
  • Plan walls: a window through a double-leaf (cavity) wall whose cavity is as wide as a leaf or wider is now found; the two leaves, flush with the glazed band's faces on both sides, count as the wall running on past it (plan_walls._collinear_run). Synthetic 0.10/0.10 and 0.10/0.15 m leaf/cavity walls went from missed to found; Clinic walls, rooms and windows are unchanged (#872).
  • Plan walls: a glazed band that holds another, thinner parallel wall (a box, chase or pier lining built against the wall) is no longer called a window (plan_walls._wall_inside_band); walls and rooms are unchanged. Clinic windows (plumbing only): first floor 23 to 22 found, precision 0.957 to 1.0; second floor 37 to 36, precision 0.973 to 1.0; recall stays 1.0 (#868).

Added

  • Semi-exterior envelope: each wall two spaces share is classified from their ASHRAE 90.1-2019 Section 3.2 categories (Space.conditioning, #895). Conditioned against semiheated or unconditioned is semi-exterior envelope (Figure 5.5.2) and lands on BuildingModel.semi_exterior, with the space pair, both categories, shared length and segments. A wall that would be semi-exterior if an unsettled side (category review, or no zone found) turned out conditioned gets a semi_exterior_envelope review item instead of a default. Two spaces with no category stay silent, and semiheated against unconditioned is not semi-exterior. The gbXML and constructions don't read it yet, so exports are unchanged (semi_exterior.semi_exterior_boundaries, real_set._semi_exterior) (#747).
  • Door detection: a door hung beside a wall that meets its hinge jamb is found when its open leaf is drawn on that wall's face (door_detect._face_leaf); the arc, jamb and clear-opening checks are unchanged. Clinic doors (plumbing only): recall 0.932 to 0.946 on the first floor, 0.896 to 0.917 on the second, precision unchanged (#743).
  • Clinic validation: scripts/validate_clinic_walls.py --doors draws each IfcDoor opening as a leaf-and-arc swing on a 50 px/m image of the cut walls and scores door_detect against the IFC doors (recall, precision; --png-dir saves the sheets). An opening wider than one leaf is drawn as a pair of leaves. Plumbing only: we choose how the swing is drawn (#743).
  • HVAC detection: the tight grille passes and the confirm gate also try the return grille drawn as a box of parallel blades with no diagonal (hvac_trace.TEMPLATE_STYLES, glyph synth.mech._ALT_GLYPH_FN), so a grille in that common drafting style is found and kept as a grille. Diffusers keep one style (#745).
  • HVAC detection: diffuser and grille templates are also tried at 0.87x and 1.15x (hvac_trace.TEMPLATE_SCALES) for proposals, the tight relabel passes and the confirm gate, so a terminal drawn up to 15% smaller or larger than ours is still found (#745).
  • HVAC detection: the stub-less grille template used to relabel and confirm grilles is now also tried at a quarter turn, so a grille whose diagonal runs the other way is kept as a grille instead of being dropped or left labeled diffuser. The diffuser X looks the same turned and keeps one template (hvac_trace.TIGHT_TURNS, #745).
  • Set runs: supply diffusers, grilles and thermostats found from a mechanical sheet's own legend now reach the HVAC model. A symbol placed in a room goes on the space (hvac.diffusers / hvac.sensors) and on its zone when exactly one zone serves that room (method legend_symbol); rooms with no zone or several zones keep the counts in symbols_by_room only, and a symbol repeated on a second sheet of the level is not added twice. A zone on a level read this way that gets no supply diffuser goes to review as zone_no_diffuser (#744).
  • Clinic detection check: --vary-only NAMES (with --vary SEED) draws only the named variations (scale, weight, turn, tilt, skew, style, knockout, text, scan, shift), each at the value the seed gives it with all of them on, so the recall loss can be put down to one variation at a time (#745).
  • Clinic detection check: scripts/validate_clinic_hvac_detection.py --vary SEED redraws the terminals the way another firm or a scan would (scale +-15%, line weight 2-4 px, a quarter turn plus up to 5 degrees tilt, slight shear, a four-way-arrow diffuser and a hatched grille as alternative styles, duct and tag text over half the symbols, then scan noise, JPEG and a 0-2 px raster shift). Seeded and deterministic; the clean render stays the default and is unchanged, and no detector threshold changes (#745).
  • Set runs: legend symbol hits on a mechanical plan that registers to its level's architectural plan are put in the room they fall in; each hvac_legends entry gains symbols_by_room (space id -> class -> count, "" for no single room) and the note says how many landed in rooms. Mechanical plan registration is now one helper shared with equipment tag placement (#744).
  • Set runs: each mapped row of a mechanical symbol legend is cut from the sheet's own raster as a one-shot template and matched across that sheet (hvac_legend.legend_templates, hvac_trace.detect_legend_symbols, NCC accept 0.80, no WiSARD pass), so a firm's own diffuser or thermostat symbols are found where matchline's built-in glyphs propose none. Each legend on the report gains symbol_hits (class -> count); the counts are not yet joined to zones. hvac_trace.merge_legend_detections keeps the built-in templates as the fallback for classes the legend does not draw (#744).
  • Set runs: the symbol legend on each vector mechanical sheet (a SYMBOL LEGEND / HVAC LEGEND / SYMBOLS title over rows of a drawn symbol and its description) is read by the new hvac_legend module. Each row's description is mapped to an HVAC detector class (supply diffuser, return/exhaust grille, VAV, AHU, thermostat/sensor) from its words only; rows go on the report under hvac_legends with the symbol's box, and rows naming no class go to one hvac_legend_unmapped review item per legend. The symbols are not yet used for detection (#744).
  • Set runs: an HVAC zone made from a placed VAV or fan coil now carries its terminal unit's scheduled design airflow (Zone.design_airflow_max_m3s / design_airflow_min_m3s, converted to m3/s from the schedule's MAX/MIN CFM or L/S columns; a fan coil's single airflow column is its max). Airflow columns whose headers state no unit (AIRFLOW alone), a max that is not positive, or a min above the max are kept on the schedule record but not put on the zone; the zone's provenance note says which. Mechanical schedule records gain airflow_unit, and MAX/MIN L/S columns are now read as max/min (#746).
  • Set runs: a door drawn inside a storefront run is modelled as a door when the storefront's window row is as wide as the run less its doors (the row is then the glass alone, centred on the glass that is left); a door no scheduled door explains goes to review. A window row as wide as the whole run still includes its doors. Before, such a storefront went to review whole and nothing was modelled (#793).
  • Plan walls: a door drawn inside full-length glazing with no gap in the wall (two jamb ticks with a single or double swing between them) no longer counts its jambs as mullions, so a cavity line with a door is not a false storefront; a storefront window that still qualifies lists the door under doors_in_glazing (#793).
  • Plan walls: a thin wall band between thicker walls whose face lines are on a glazing CAD layer (such as A-GLAZ) is a window over its whole length (glazing_layer_band, confidence 0.7) instead of a maybe_glazing review question; on any other layer it stays flagged as before (#793).
  • PDF CAD layers: ingestion records each vector path's optional content group (layer) name and each sheet's layer list. On plans, glazing that runs a whole wall counts as a window (glazing_layer, confidence 0.7) when it sits on a glazing layer such as A-GLAZ, with or without mullion ticks, and a middle line on a pattern or insulation layer such as A-WALL-PATT is never a window (#793).
  • Lighting power per room on a drawing set: lighting schedule tags on a reflected ceiling plan or electrical floor plan are counted as fixtures in the room that contains them, giving Space.lighting fixtures, watts and LPD (one tag counted as one fixture, so each lit level goes to review); drawing lighting now reaches the gbXML LightPowerPerArea on the set path, which it did not before (#746).
  • Plan walls: a thinner wall on the same line filling at least 80% of a gap in a thick wall run stops the run there, so a glazed bay narrower than MAX_OPENING_M (2.5 m) is kept as its own wall and flagged maybe_glazing instead of being covered by one continuous opaque wall (#793).
  • Heated slab: when the radiant floor rows name the ground-floor rooms they serve, only those rooms' exposed slab edge gets the Table 5.5 heated F-factor and the rest the unheated one (slab_heated_spaces); otherwise the whole slab stays heated (#747).
  • Plan walls: a thin wall band between thicker walls on the same line, with no opening or scheduled tag, is flagged maybe_glazing and goes to review as possible storefront glazing; the wall stays opaque (#793).
  • Slab-on-grade exposed perimeter counts every ground-floor wing (not only the largest) and the edge of open-air courtyards, a hole in the slab with exterior walls along at least half its edge; other holes stay slab and are listed under holes_not_exposed (#747).
  • Schedules drawn as whitespace-aligned text with no cell lines are read (pdf_unruled_table, confidence 0.75): columns from the header row, wrapped cells and note rows handled, crossing values and repeated tags flagged unparsed (#746).
  • Plan walls that stop 2.5 to 4.5 m short of the wall ahead close with a reviewed air wall when that parts two labelled rooms (counters, half walls between named spaces); inside one named space they stay open (#740).
  • Each placed VAV or fan-coil tag on a set run becomes an HVAC zone on the room it sits in (#746).
  • Scheduled mechanical equipment tags on the mechanical plans are placed in rooms, registered to the architectural plan by the shared grid or the same sheet frame (#746).
  • Wall-type legend descriptions wrapped onto several lines are read whole; the first line still sets the class when it names one (#828).
  • Plan openings keep every nearby tag (tags_near); the nearest scheduled door/window mark picks the schedule row even when a wall-type tag sits closer, and the wall type still applies to the wall (#829).
  • Drawing-set runs read the wall-type legend (W1 8" CMU ...) and give each tagged exterior wall a construction named by it; the library fills its U from Table 5.5 for that class instead of the unlabeled default. Conflicting legends and walls with two types go to review (#747).
  • A scheduled window tag (e.g. SF-1) on a plan wall with no opening drawn: when its scheduled width spans the wall it is modelled as a full-length storefront window (plan_wall_tag, confidence 0.7); a narrower tagged door or window there goes to review as a possible undrawn opening. walls_NNN.json gains wall_tags (#793).
  • A facade drawn in parts (two sheets, or titles like "SOUTH ELEVATION - EAST HALF") is joined as a whole: each part is placed by shared grid labels or the end its title names, covers its own span, and a plan opening goes to review only when no part shows it. An opening two elevations draw differently goes to review once, naming both sheets (#818).
  • Run config wall_height takes a per-level map ({L1: 4.5, L2: 3.6}) for drawing-set runs; named levels use it and the rest fall through to the elevation level marks. Config wall_height now reaches the drawing-set run at all (before, only --storey-height did); unknown level ids fail the run (#821).

Added

  • Level marks match more plan levels: GROUND then FIRST FLOOR on one elevation reads the British way, MEZZANINE and PENTHOUSE marks match MEZZ and PH, and marks that name no plan level match by order when the storey count agrees (height_match per level) (#822).

Added

  • Each level takes its own storey height from the elevation level marks (matched by name: FIRST FLOOR to L1, SECOND FLOOR to L2); level elevations are the running sum, and only levels whose marks disagree or are missing keep the default and go to rq-storey-height (#814).

Changed

  • Review edits on space-merge items change the model (#799): merge into <room> (or a bare space id / room number) merges the kept space into the room the reviewer picked, validated against the same rules as the automatic merge (same level, outlines must join); the merge snapshot is kept on the item so revert restores both spaces, zones, openings, lighting, HVAC, daylight and envelope walls, even after save/reload.
  • Elevation join review items use the window kinds: size mismatches and plan openings missing from the elevation are elevation_conflict, elevation openings with no plan opening are window_room_link; ids, targets and triage task unchanged (#817).

Added

  • Review page draws the elevation sheets the drawing-set run read (facade outline, windows and doors joined or not), and an item that joins two sheets highlights both ends, labels each with the other sheet, and offers a bar to jump between them (#801).

Added

  • Elevation join review items name both sheets (target.ends with the elevation box and the plan point), and elevation JSON records each opening's matched plan opening (#810, third slice).

Added

  • Storey height comes from elevation level marks when they agree (checked against where each mark is drawn); disagreeing marks keep the default and go to review (#810, second slice).

Added

  • Elevation openings join the plan's: matched windows and doors get their sill and head height from the elevation, schedule size kept; disagreements and unmatched openings go to review (#810, second slice).

Added

  • Drawing-set run reads elevation sheets: windows and doors with sill/head heights, registered to the plan facade by shared grid labels or outline length; unnamed or mismatched elevations go to review (#810, first slice).

Added

  • Plan windows: full-length or extra-wide glazing broken by at least two mullion ticks is a window (source: "glazing_mullions", confidence 0.6); a cavity line along the whole wall still is not (#793).
  • Plan openings: a schedule tag written next to a plan gap picks its schedule row, even where same-width rows disagree; a contradicting tag goes to review (#793).
  • CI: a browser-level test drives the review page in headless Chromium and replays its exported decisions with matchline review --apply (#802).
  • Review: an edit on an opening_unsized item adds the opening the pipeline left out, from a schedule tag or typed sizes, checked against the drawn gap before anything changes; revert, confirm or reject remove it (#798).

Changed

  • gbXML export now writes and validates against schema 8.01 (was 6.01), using a local copy of the 8.01 xsd whose versionEnum adds 7.03 and 8.01; the published 8.01 schema stops at 6.01 and rejects its own version (#804).
  • Renamed the project from wisard-bem to Matchline: distribution name, GitHub repo (anchapin/matchline), docs, and the matchline CLI. The old wisard-bem alias was removed in 0.2.0. The WiSARD paper-reproduction module (jesse.py) keeps its name.

Added

  • pyproject.toml: the project is installable (pip install -e .), version 0.1.0, with declared runtime dependencies and optional ocr, test, and detector extras.
  • Unified matchline CLI with subcommands for every demo script (validate, bem-export, elevation-windows, facade-takeoff, room-labels, multidiscipline, mnist, symbols, ifc-import).
  • GitHub Actions CI: install, ruff check, ruff format --check, pytest on push/PR to develop and main.
  • Ruff lint/format config, .pre-commit-config.yaml.
  • CONTRIBUTING.md, RELEASING.md, SECURITY.md, issue/PR templates, docs/README.md index.
  • geometry_simplify now raises a clear ImportError (install hint) instead of falling back to a machine-specific vendored shapely.

Added (late entries for this release)

  • Explicit area definitions (GSF, BOMA rentable, program/assignable) with reconciliation (#975).
  • Mechanical schedule parsing and system-type classification (#974).
  • Match lines, split sheets, and enlarged-plan deduplication (#994); legacy plan sheets route by plan-like geometry with a single-dimension scale fallback (#1016, #1019).
  • Review viewer: a read-only 3D view with pan/rotate and level-by-level top-down (#1001), in-viewer corrections (rename, reclass, merge, split, revert-to-auto) (#1002), MEP takeoff and trace overlays (#1004), and a provenance-method filter chip in the queue (#1080).
  • Review queue: worst-first ordering, level tracking, filters and bulk actions (#996).
  • Correction history with undo and revert-to-automatic (#997); revision/addenda tracking and takeoff diffs, including moved-opening pairs and space-identity fingerprints (#993, #1034, #1042-#1048), a --baseline flag on matchline run (#1077), and diff results fed into the review queue (#1048).
  • Export package as one zip with a self-contained share-back preview, and a feedback-log summary in the trust report (#999, #1081).
  • Progressive takeoff mode: stop after takeoff, skip BEM build and export (#998).
  • Per-project legend tag learning with a sidecar, classify override and before/after eval (#995).
  • Versioned minimum-telemetry spec with allowlist, exclusion list and consent gates, the allowlist emitter wired into the run pipeline, and in-product feedback capture wired into review decisions, stage failures and the CLI (#1003, #1037, #1038).
  • Real-building benchmark expanded to 12 licensed buildings; the scoreboard gains real simplification and surface-count columns, door recall via IfcDoor openings, and raw entity-count truth (#748, #972).
  • Wheel packaging: pip install matchline works as a real install - every module ships in the wheel (review_classifier and its runtime data), the CI wheel check reads packages from pyproject, and the synth e2e scripts stay out of the wheel (#1035, #1070, #1071, #1076, #1081).
  • matchline run reaches the --set and --ifc input paths end to end (#1036).
  • Docs: contributor review norms and a Code of Conduct (#1092), a generated repo map with a CI drift check and a complete docs index (#1096), takeoff-diff and learn-legend documented in the README (#1066).
  • Publishing: a tag-driven PyPI release workflow with trusted publishing; pushing a vX.Y.Z tag builds, smoke-tests and publishes. This release is the first publish (#1099).
  • CI and hygiene: the CLI test subprocess cap raised to 180s (#1072), configs and docstrings aligned with behavior and one shared point_in_polygon (#1073-#1075), the demo install exercises every CLI subcommand (#1076), orphaned elevation tests moved with a repo-root guard (#1064), dead public code removed from review_classifier (#1065), stage JSON writes go through temp-then-replace (#1039), the missing-OCR path made deterministic (#1040), one shared page head for the HTML pages (#1044), unmapped legend descriptions suggest --tag mappings (#1047), takeoff-diff classifies each space once (#1043).
  • Dependencies: Renovate bumps (ruff 0.17, GitHub Actions v7, CPython 3.15 with a deferral in ci.yml).

[0.1.0] - 2026-09-19

Initial public development snapshot on develop.

Added

  • IFC import: dims fallbacks for real-file geometry the kernel cannot process - closed-profile bbox for IfcExtrudedAreaSolid with line-only IfcIndexedPolyCurve/IfcPolyline outer curves, and vertex bbox for IfcPolygonalFaceSet; plus IfcIndexedPolyCurve/IfcPolyline footprints in _polyline_footprint so space and wall extraction survive TUM GNI anonymized exports (#748).
  • WiSARD classifier reproduction (thermometer encoding, empirical-Bayes normalization, COM normalization, skeletonization): 94.57% on MNIST.
  • Drawing pipeline: symbol/schedule takeoffs (count × width × height), OCR room labeling, area-budgeted geometry simplification (≤2% drift default), lighting takeoffs + LPD, HVAC zoning from duct tracing.
  • Exact elevation windows: along-wall position, sill/head heights, daylight zones, two-elevation dedup, reconciliation review queue.
  • CMP Facade takeoffs: wall/glazing/door fractions + dataset priors.
  • Canonical BuildingModel: architectural spaces as the canonical entity, provenance on every fact, review queue, cross-discipline linking.
  • 28-check validation battery; errors block export.
  • gbXML 6.01 + IFC4 export (schema/round-trip validated).
  • Tier-0 IFC import frontend: IFC4 → canonical model without space boundaries (spaces, elements, openings, material layers, zones).
  • Detector track: YOLO dataset converters (CubiCasa/AEC/FloorPlanCAD), training harness, SAHI tiling inference.
  • ROADMAP.md: seven hard problems with initial technical framing (wall thickness, sloped roofs, skylights, non-room polygons, shading, per-segment constructions, IFC frontend).
  • BSD-3-Clause license.