S8.4 — Non-residential programs, office plate and cafeteria Implementation Plan¶
Status: Done (controller, D-098; pending the owner's reading) · Date: 2026-10-02 · Roadmap: S8.4 (section S8, Typologies) · Decisions: D-023, D-026, D-061, D-064, D-065, D-079, D-085, D-087, D-090, D-091, D-093, D-094, D-095, D-096, D-097, D-098; ADR-006 (Tasks 4 and 10), ADR-011, ADR-014 (new, Task 2;
docs/decisions/ADR-014-program-types-and-department-zoning.md)For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Implement S8.4 of D-098: pin and paraphrase the four-source family set SYN-TYP-001 to SYN-TYP-019 (D-093) on a new precedent page; widen the research brief, the repository spec, README, AGENTS.md, and the roadmap to building typologies of every program type (D-094), with the sub-stages S8.4–S8.7 before S6; record in a new ADR-014 the non-residential space types, one typed illustrative preset for each (D-023, D-061), and which rooms form one department zone in every new family (D-097); specify all nine new families in ADR-006 with controller design defaults (D-098); add the twelve new SpaceType values and fifteen example presets (ExampleNonResidentialPresets) with their default preset components in 1 Program (D-065) and icons; and build the first two non-residential families, SYN-TYP-017 (Office Plate, an office work area that is one ring-shaped zone around a central service core) and SYN-TYP-016 (Cafeteria, a kitchen and servery edge along the north of a larger dining field), with their Grasshopper components and icons. The office ring is the first zone with a hole in a footprint without one; the surface builder, every plan simplifier, and every floor aggregator already handle it, and both families validate with every simplifier and aggregator on their canonical plans and rotated and moved by two placements. Every existing plan, floor, building, and snapshot stays unchanged. S8.4 ends with version 0.8.3 and the tag v0.8.3; after the stage gate and a fresh clone of the tag pass scripts/verify.ps1, the controller pushes main and v0.8.3 (D-098).
Architecture: The documents come first (Slice A): docs/research/precedents/syn-typologies.md paraphrases the pinned records, ADR-014 sets the program types and department zoning, the scope documents are widened, and ADR-006 gains the section Families of the four-source set (S8.4–S8.7). In Lod.Core the SpaceType enum gains twelve values appended after Mixed, so every existing value keeps its number; ExampleNonResidentialPresets follows ExampleResidentialPresets (S4.1): one PresetValues and one ProgramPreset per space type, plus SpaceTypes, ValuesOf, and PresetOf. In Lod.Grasshopper fifteen DefaultPresetComponent subclasses expose the presets, and PreviewGoo gives each new space type a preview colour. The new families are folders Lod.Generators/OfficePlate/ and Lod.Generators/Cafeteria/, each with a parameter record (a static Default holding the ADR-006 defaults), a preset record with one slot per space type, and a generator on the PlanGenerator base; the office work area is Polygon2.Create(plate, new[] { core }), a zone with the core as its hole inside a footprint without one. Two components derive from the S8.1 TypologyGeneratorComponent, and scripts/make_icons.py draws seventeen new icons.
Tech Stack: C# 12 (LangVersion 12.0); .NET SDK ≥ 8 (global.json pins 8.0.100 with rollForward: latestMajor); netstandard2.0 core libraries; Clipper2 2.0.0; RhinoCommon and Grasshopper 8.19.25132.1001 (plugin targets net7.0); xUnit 2.9.3, xunit.runner.visualstudio 3.1.5, Microsoft.NET.Test.Sdk 17.14.1 (tests target net8.0); Windows PowerShell 5.1 for scripts/verify.ps1; Python 3.10 or later with Pillow for scripts/make_icons.py (the icons in this plan were drawn with Pillow 12.3.0).
Global Constraints¶
- Gate: D-093 to D-098 are recorded on
main(2e3d3b1), and this plan is committed tomainbefore Task 1 ("docs(plans): add the s8.4 non-residential programs plan"); it is the only file of that commit. S8.4 depends on S8.3, taggedv0.8.2, and on5038512(Perimeter Core rejects self-intersecting offset rings, two integration tests), which is onmainafter the tag. ADR-006 and ADR-011 were accepted in S8.1; ADR-014 is written in Task 2 and accepted by the controller under D-098, pending the owner's reading. Before Task 1 the working tree is clean (git status --shortprints nothing). - Rhino 8 (8.19 or later) only (D-003); stages exchange Grasshopper objects, not JSON (D-017).
Lod.CoreandLod.Generatorshave no RhinoCommon or Grasshopper reference; their tests run with plaindotnet test.- Nullable reference types on; warnings are errors;
.editorconfigrules are enforced in the build; every public member ofLod.CoreandLod.Generatorshas XML docs (CS1591 is an error). - Generation is deterministic; outputs have a stable order: the office plate lists
SC(the core), thenOF(the office); the cafeteria listsKS(kitchen and servery), thenDI(dining);ExampleNonResidentialPresets.SpaceTypeslists the fifteen types in the order of ADR-014. - Space types are only appended. The twelve new
SpaceTypevalues followMixedin the orderOffice,Retail,Mall,Kitchen,Dining,OperatingTheatre,CleanCorridor,DirtyCorridor,ClinicalSupport,CareBedroom,CareCommunal,ActivityHall; no existing value is renamed, removed, or moved, andCore,Lobby, andServiceare reused with clarified documentation (ADR-014). Semantics stay explicit data, never inferred from names (D-094). - Illustrative presets (D-023). The fifteen presets hold illustrative, documented values chosen by the controller (ADR-014), not DOE prototype or standard values, and the remark of every default preset component says so. Every preset is conditioned; ventilation (
LoadType.Ventilation, air changes per hour) is given only for the kitchen and the operating theatre; no preset defines domestic hot water. - Numerical tolerances come only from
ToleranceSettings(ADR-004); S8.4 adds none and changes none.OfficePlateGenerator.MinimumOfficeDepth(3 m) is a rule of the family (ADR-006: the work area surrounds the core, so a core near a façade is outside the family), like a generator's minimum count, not a tolerance; the depth is compared without a tolerance (exactly 3 m is valid).ToleranceSettings.csis not touched. The minimum wall and window geometry stays inGeometryLimits(ADR-011), unchanged. - Existing outputs do not change. The sixteen existing snapshots (
linear-plan-canonical,stack-two-storeys, the threesimplifier-*, the threeaggregator-*, the S8.1 snapshotsstair-bay-bar-canonical,stair-pair-canonical,gallery-bar-canonical,terrace-row-canonical, the S8.2 snapshotspoint-plate-canonical,winged-band-canonical,open-court-canonical, and the S8.3 snapshotenclosed-court-canonical) stay byte-identical, and every existing test keeps passing; no test is removed or changed, and no existing icon PNG changes. - Never weaken a check (AGENTS.md rules 11, 12). Tasks 5, 7, and 8 start from tests that fail the way the task says; the integration matrices of Tasks 7 and 8 pass at once, because every simplifier and aggregator already handles the new plans (each task says to stop if a case fails). No tolerance is widened, no validation check is relaxed, and no test is weakened or removed.
- Report, never repair (GLOBAL.md quality rule 6). A length that is not positive and finite, a core that leaves less than 3 m of office on a side, and a kitchen not shallower than the dining field are the error
InvalidParameter, every one reported at once. A plan that Perimeter Core cannot zone fails withPlanTooNarrow(D-087), never repaired; the generators do not impose that precondition. Zones about 1 m wide on rotated plans remain the known limit of D-090; S8.4 does not change it. - Typed records per family (AGENTS.md): each new family has its own parameter and preset records; there is no catch-all parameter class and no dictionary of parameters. The office plate's presets are
OfficeandCore, the cafeteria'sKitchenandDining, each checked against its slot's space type (PresetSpaceTypeMismatch). - Department zoning (D-097). A non-residential zone is one department, a contiguous group of rooms of one program, never a room or a tenancy: the office work area is one zone (a ring) and the core another; the kitchen with its servery is one zone and the dining field another; the servery counter and the customers' route along it are not zones. Housing keeps one zone per dwelling and storey (D-009, D-085).
- Source contract (D-093). The records are read from
typology-synthesizerat tagv0.3.1-research(7538ed3), whosedata/four_source/typologies.jsonl,generators.jsonl, ands6_validation.jsonlhave the SHA-256 values given on the precedent page; no book text, page images, plan IDs, or copied drawings enter BEMGen, and the page paraphrases each family with its stable ID. The source repository is not opened or modified while this plan is executed; every text the tasks need is given here. - No backward compatibility (D-064). Seventeen components are added with new GUIDs: the preset components Office Preset
4160f791-07dc-4529-9986-1c9ba7d21a98, Core Presetca44b061-1ced-4ae3-afed-d806e6db2b7c, Retail Presetb760860d-05c9-4cbe-a22f-ddd2e38e4d64, Mall Presetdbd514d5-7553-4000-b10c-bca748e3b301, Kitchen Preset658e05dc-c935-4518-bc99-512546153e1e, Dining Presetc8b05fa2-330a-4e8a-acaa-e57ca57b4fd9, Operating Theatre Preseteb55b6d5-08f1-4a5e-9598-13951c34c2a4, Clean Corridor Presetc199a986-28b8-41c7-b260-5f4581b7362b, Dirty Corridor Preset0e61c86e-5981-4a11-b922-52650757e4a9, Clinical Support Preset274e0ac9-de95-4bd4-8028-25d74bfd1e4b, Care Bedroom Preset8447ea38-faec-4630-96dd-92569cd7d0d8, Care Communal Preseta3000f8c-633f-4a54-9fb8-1a4109e337bb, Lobby Preset395aebb3-3e49-4814-93ab-388da9de2360, Activity Hall Preset6bf00cd3-70b9-4337-8b80-70fc00d79666, Service Preset60251c8e-ad4b-4ef5-8f68-64f4c6bf6f8b, and the generators Office Plate783cefaa-b836-4dbc-b16d-8e74820ed361and Cafeteria95956724-ddbc-48ce-9874-0ec6c244c8fe. No existing component, input, output, or GUID changes. - Grasshopper components are thin adaptors (AGENTS.md): every rule lives in
Lod.CoreorLod.Generators; a component reads inputs, calls the generator or builds the preset, and reports its diagnostics. - Research equivalence rules are not changed (GLOBAL.md scientific rule 7): a new space type is a program like any other; its loads, schedules, setpoints, and glazing are aggregated by the existing rules (ventilation, like infiltration, per air change and volume weighted, D-019). The research brief and the repository spec change only in scope (D-094, Task 3).
- No simulation inside the pipeline (D-016). No CI (D-005):
scripts/verify.ps1passes locally before every merge tomain. - Commits:
type(scope): summaryin lower case, no agent, model, or tool attribution and noCo-Authored-Bytrailers (D-001, D-002); author identity per D-050. - Pushes (D-098). Slices are rebased onto local
mainand nothing is pushed while the stage runs. After the close-out (Task 12), its tag, and a fresh clone ofv0.8.3that passesscripts/verify.ps1, the controller pushesmainandv0.8.3(Task 13); never a force push. .ghexample definitions are made and saved by a person in Rhino 8; agents never write or check them (D-058). S8.4 adds none.- S8.4 changes these earlier files:
ZoneProgram.cs(S1),PresetComponents.cs(S4.1),BemGoo.cs(S2, last changed in S4.2),TypologyGeneratorComponents.cs(S8.1, last changed in S8.3),scripts/make_icons.py(S4.1, last changed in S8.3), andDirectory.Build.props(S0, last changed in S8.3); and the documents AGENTS.md and README.md (last changed in S8.3), the research brief (last changed in S8.1), the repository spec (unchanged since S0), the roadmap (last changed by436e415afterv0.8.2), ADR-006 (S8.1) and ADR-002 (both last changed in S8.3),docs/architecture/convert2bem.md,pipeline.md,docs/research/program-presets.md,docs/research/precedents/hdhm-typologies.md, anddocs/development/grasshopper-smoke-test.md(last changed in S8.3),domain-model.md(last changed in S8.1), the decision log, and one link in each of the S8.1, S8.2, and S8.3 plans (Task 3).
Files¶
| Path | Change | Responsibility |
|---|---|---|
docs/research/precedents/syn-typologies.md |
Create | The pinned four-source records paraphrased: 19 families, lineage, support, generator and sub-stage of each, the ID rule |
docs/research/precedents/hdhm-typologies.md |
Modify (S8.1, last changed in S8.3) | Note that it is the issued baseline |
docs/decisions/ADR-014-program-types-and-department-zoning.md |
Create | The new space types, the fifteen illustrative presets, the default preset components, the department zones of every new family |
AGENTS.md, README.md, docs/LOD_geometric_zoning_equivalence_research_brief.md, docs/LOD_grasshopper_plugin_repository_spec.md, docs/plans/2026-09-30-implementation-roadmap.md |
Modify | Scope of D-094; sub-stages S8.4–S8.7 before S6 (D-098) |
docs/plans/2026-10-02-s8.1-bar-typologies.md, …-s8.2-non-rectangular-typologies.md, …-s8.3-enclosed-courtyard.md |
Modify | The roadmap link follows the renamed S8 heading |
docs/decisions/ADR-006-plan-generation-mechanism.md |
Modify | Families of the four-source set (S8.4–S8.7); 017 and 016 as implemented; pipeline consequences of S8.4 |
src/Lod.Core/Programs/ZoneProgram.cs |
Modify (S1 file) | Twelve new SpaceType values appended |
src/Lod.Core/Programs/ExampleNonResidentialPresets.cs |
Create | Fifteen illustrative presets with their schedules; SpaceTypes, ValuesOf, PresetOf |
tests/Lod.Core.Tests/Programs/ExampleNonResidentialPresetsTests.cs |
Create | Enum order, values, presets, loads and schedules, retail and care schedules, ventilation |
src/Lod.Grasshopper/Components/PresetComponents.cs |
Modify (S4.1 file) | Fifteen default preset components |
src/Lod.Grasshopper/Goo/BemGoo.cs |
Modify (S2 file, last changed in S4.2) | Preview colours of the new space types |
src/Lod.Generators/OfficePlate/OfficePlateGenerator.cs |
Create | OfficePlateParameters, OfficePlatePresets, OfficePlateGenerator (017) |
tests/Lod.Generators.Tests/OfficePlate/OfficePlateGeneratorTests.cs |
Create | Defaults, the ring around the core, tiling, the core's walls, façades, windows, invalid parameters, the minimum office depth, presets, determinism, provenance, snapshot |
tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt |
Create | Canonical plan of the family |
tests/Lod.Integration.Tests/OfficePlateTypologyTests.cs |
Create | The office plate × every simplifier × every aggregator, canonical and at two placements; Semantic Merge, Perimeter Core, Single Zone Building |
src/Lod.Generators/Cafeteria/CafeteriaGenerator.cs |
Create | CafeteriaParameters, CafeteriaPresets, CafeteriaGenerator (016) |
tests/Lod.Generators.Tests/Cafeteria/CafeteriaGeneratorTests.cs |
Create | Defaults, the service edge along the dining field, tiling, windows, invalid parameters, the kitchen depth, presets, determinism, provenance, snapshot |
tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt |
Create | Canonical plan of the family |
tests/Lod.Integration.Tests/CafeteriaTypologyTests.cs |
Create | The cafeteria × every simplifier × every aggregator, canonical and at two placements; Semantic Merge, Perimeter Core |
src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs |
Modify (S8.1 file, last changed in S8.3) | Office Plate and Cafeteria |
scripts/make_icons.py |
Modify (S4.1 file, last changed in S8.3) | Seventeen new motifs (Tasks 6 and 9) |
src/Lod.Grasshopper/Icons/*PresetComponent.png (15), OfficePlateComponent.png, CafeteriaComponent.png |
Create (generated) | 53 icons |
docs/architecture/pipeline.md, domain-model.md, convert2bem.md, docs/decisions/ADR-002-geometry.md, docs/research/program-presets.md |
Modify | The presets, the generators and components, a zone with a hole in a footprint without one |
docs/development/grasshopper-smoke-test.md |
Modify | Append the S8.4 checklist (Task 10) and its result (Task 12) |
Directory.Build.props |
Modify | Version 0.8.3 |
AGENTS.md, README.md |
Modify | "Project state" and "Status" after S8.4; README layout line |
docs/plans/2026-09-30-implementation-roadmap.md |
Modify | Progress line and detailed-plans row |
docs/decisions/decision-log.md |
Modify | D-099 (SYN-TYP-* IDs), D-100 (at most four arms on the hub-and-arms layout), D-101 (S8.4 complete) |
Test counts on net8.0 at the start of S8.4: 158 core, 181 generators, 603 integration (D-092 records 601; 5038512 added two). After Tasks 1–4: 158 / 181 / 603. After Task 5 and Task 6: 193 / 181 / 603. After Task 7: 193 / 204 / 654. After Task 8 and at the end: 193 core, 224 generators, 704 integration (core: +35 ExampleNonResidentialPresetsTests; generators: +23 OfficePlateGeneratorTests, +20 CafeteriaGeneratorTests; integration: +51 OfficePlateTypologyTests, +50 CafeteriaTypologyTests). No test is removed or changed. Grasshopper code has no unit tests; Tasks 6 and 9 are checked by a 0-warning build, the embedded icons, and the smoke test of Task 11.
Task assignment (D-048, execution handoff §6)¶
| Task | Agent | Why |
|---|---|---|
| 1 Precedent page | bemgen-bounded |
Document fully given by the plan |
| 2 ADR-014 | bemgen-bounded |
ADR fully given by the plan |
| 3 Scope alignment | bemgen-bounded |
Documents, exact replacements |
| 4 ADR-006 families | bemgen-bounded |
ADR, exact replacements |
| 5 Space types and presets | bemgen-bounded |
Model types and data fully given by the plan, test-first |
| 6 Default preset components and icons | bemgen-complex |
Thin components, but generated binaries, the Pillow toolchain, embedded resources, and a visual check (as S4.1 Task 7) |
| 7 Office plate (017) | bemgen-complex |
New plan generator geometry, the first zone with a hole in a footprint without one; new snapshot; combination matrix |
| 8 Cafeteria (016) | bemgen-complex |
New plan generator geometry; new snapshot; combination matrix |
| 9 Generator components and icons | bemgen-complex |
Thin components with generated icons and a visual check |
| 10 Architecture documents and smoke-test checklist | bemgen-bounded |
Documents |
| 11 Smoke test | The controller headless (D-056); person for the viewport items | Needs Rhino 8 |
| 12 Close-out | bemgen-bounded |
Bookkeeping |
| 13 Fresh-clone gate and push | Controller (D-098) | A push is never delegated |
The end-of-stage review is done by bemgen-complex. A bounded task escalates to bemgen-complex after two failures or any deviation from this plan's expected output.
Slice A — docs/four-source-scope¶
The documents come first, as the brief of D-098 orders them: the precedent page (Task 1), ADR-014 (Task 2), the scope alignment of D-094 (Task 3), and ADR-006's specification of the new families (Task 4). ADR-014 comes before the scope alignment because AGENTS.md, the repository spec, and the roadmap link to it. No code changes in this slice; every task ends with the same counts, 158 core, 181 generators, and 603 integration tests.
Task 1: The four-source families paraphrased on a precedent page¶
Files:
- Create: docs/research/precedents/syn-typologies.md
- Modify: docs/research/precedents/hdhm-typologies.md (S8.1, last changed in S8.3)
Interfaces:
- Consumes: D-093 (the pin), D-094, D-096, D-097, D-098; the records of typology-synthesizer at v0.3.1-research (7538ed3), paraphrased here and not read during execution.
- Produces: the page that ADR-006, ADR-014, AGENTS.md, and the roadmap cite for the families: the source contract with the tag, commit, and SHA-256 of typologies.jsonl, generators.jsonl, and s6_validation.jsonl; the rule that BEMGen documents use the SYN-TYP-* IDs with the HDHM-TYP-* lineage for 001–009 (recorded as D-099 at the close-out); the support labels; an overview table of the 19 families with lineage, support per book and in the corpus, the BEMGen generator, and the sub-stage; what changes for 001–009; a paraphrase of 010–019 (013 set aside, D-096); and the plans outside every family. hdhm-typologies.md gains one note that it is the issued baseline.
- [x] Step 1: Create the branch
git switch -c docs/four-source-scope main
- [x] Step 2: Create
docs/research/precedents/syn-typologies.md(full file)
# Four-source typology families (`SYN-TYP-*`)
The precedent set of the plan generators (D-093) is the active four-source family set of the research repository `typology-synthesizer`: 19 families `SYN-TYP-001` to `SYN-TYP-019`, drawn from four books, *High-Density Housing* (DETAIL, `HDHM`), *Floor Plan Manual Housing* (fifth edition, `FPMH`), *Architects' Data* (second international English edition, `AD`), and *Metric Handbook: Planning and Design Data* (second edition, `MH`). The set covers housing and non-residential programs alike (D-094). This page paraphrases the families in BEMGen's words, with their stable family and rule IDs, gives their lineage from the nine *High-Density Housing* families of S8 ([hdhm-typologies.md](hdhm-typologies.md)), and names the BEMGen generator that realises each one and the sub-stage that builds it (D-098). It contains no book text, no page images, no copied drawings, and no claim, plan, or project IDs, which lead back to the books and stay in the source repository.
## Source contract
| Item | Value |
| --- | --- |
| Repository | `typology-synthesizer` (local path `D:\typology-synthesizer`, read-only for BEMGen) |
| Tag | `v0.3.1-research` |
| Commit | `7538ed3` |
| Records version | 3.0.0 (`data/four_source/`) |
| `data/four_source/typologies.jsonl` | SHA-256 `3f0aa78a9b53cc91fe6d47554706374a548f46afe70f15a056984675c8833f65` |
| `data/four_source/generators.jsonl` | SHA-256 `9321ef1f14cfe39e6e103946b490e535283e3a0df8c4200b217c5faac086a56d` |
| `data/four_source/s6_validation.jsonl` | SHA-256 `94f5948e979d88193966a1a715857b6afdf373aa3a40aaba47caff68231f9129` |
| Read alongside | `docs/typologies/four-source.md`, `docs/typologies/integrated.md`, `docs/generator-specs/four-source-handoff.md` |
- BEMGen reads the records only at that tag and commit; nothing in the source repository is modified (D-093).
- Family IDs are `SYN-TYP-001` to `SYN-TYP-019`; rule IDs are `SYN-TYP-0nn-R00k`, in the order the source gives them (written `R00k` below where the family is clear).
- **IDs in BEMGen documents.** From S8.4 on, BEMGen documents name the families by their `SYN-TYP-*` IDs and give the `HDHM-TYP-*` lineage for `001` to `009`. Component descriptions written in S8.1–S8.3 may keep their `HDHM-TYP-*` IDs as long as the `SYN-TYP-*` ID is also given. This is a provisional controller decision of S8.4 (decision log).
## Labels
- **Support within a source** (`recurrent_in_source`): the family's relationships recur among several independently identified, named projects of that one book. *Provisional*: one project of that book only; usable as a design hypothesis without independent confirmation. A label says nothing beyond the book it is given for.
- **Support in the corpus** (`recurrent_in_corpus`): the family is supported by independent projects across the four books taken together; *provisional* otherwise. It claims no representativeness beyond these four books.
- **Topological only.** The rules name building parts and how they connect (which part serves which, which parts adjoin, whether a court is open, which route reaches which room). No source supports a metric or count range for any family: the only observed counts (core count, courtyard count) have no candidate bounds. Every dimension, count, room size, service provision, and façade choice in a BEMGen generator is a design input with a default chosen in [ADR-006](../../decisions/ADR-006-plan-generation-mechanism.md) (D-083, D-098).
- Every family has a *constraint* rule (keep all its essential relationships when other properties vary) and a *failure condition* (an organisation without one of them is not a member). BEMGen realises both through parameter validation: a parameter value that would remove an essential relationship is the error `InvalidParameter` (ADR-006).
- BEMGen generators are not tested against the source's symbolic validation records (`s6_validation.jsonl`; D-083); ADR-006 states in text how each generator realises its family's rules, citing these IDs. Which rooms of a non-residential family form one zone is set in [ADR-014](../../decisions/ADR-014-program-types-and-department-zoning.md) (D-097).
## Overview
| Family | Organisation | Lineage | Support within a source | Corpus | BEMGen generator | Sub-stage |
| --- | --- | --- | --- | --- | --- | --- |
| `SYN-TYP-001` | Repeated stair bays in a linear bar | `HDHM-TYP-001`, carried | HDHM recurrent (4 projects) | recurrent | `StairBayBarGenerator` (*Stair Bay Bar*) | S8.1 |
| `SYN-TYP-002` | Continuous external gallery beside a linear bar | `HDHM-TYP-002`, revised (more examples) | HDHM recurrent (5), FPMH recurrent (3), AD provisional (1) | recurrent | `GalleryBarGenerator` (*Gallery Bar*), gallery not modelled | S8.1 |
| `SYN-TYP-003` | Projecting wings joined by a common band | `HDHM-TYP-003`, carried | HDHM recurrent (3), FPMH provisional (1) | recurrent | `WingedBandGenerator` (*Winged Band*) | S8.2 |
| `SYN-TYP-004` | Open U-shaped court with stair-served wings | `HDHM-TYP-004`, carried | HDHM recurrent (2), FPMH provisional (1) | recurrent | `OpenCourtGenerator` (*Open Court*) | S8.2 |
| `SYN-TYP-005` | Enclosed courtyard residential block | `HDHM-TYP-005`, carried | HDHM provisional (1), FPMH provisional (1) | recurrent | `EnclosedCourtGenerator` (*Enclosed Court*) | S8.3 |
| `SYN-TYP-006` | Attached narrow terrace row | `HDHM-TYP-006`, carried | HDHM provisional (1), FPMH recurrent (2) | recurrent | `TerraceRowGenerator` (*Terrace Row*) | S8.1 |
| `SYN-TYP-007` | Compact point-access tower plate | `HDHM-TYP-007`, revised (more examples) | HDHM provisional (1), FPMH recurrent (2), AD recurrent (3) | recurrent | `PointPlateGenerator` (*Point Plate*) | S8.2 |
| `SYN-TYP-008` | One stair between a dwelling pair | `HDHM-TYP-008`, carried | HDHM provisional (1) | provisional | `StairPairGenerator` (*Stair Pair*) | S8.1 |
| `SYN-TYP-009` | Double-sided internal corridor bar | `HDHM-TYP-009`, carried | HDHM provisional (1), FPMH provisional (1) | recurrent | `LinearPlanGenerator` (*Linear Plan Generator*, since S2) | S8.1 (check) |
| `SYN-TYP-010` | Radial lobes around a point tower core | new | FPMH provisional (1) | provisional | `RadialLobesGenerator` (specified) | S8.5 |
| `SYN-TYP-011` | Linked housing clusters around multiple courts | new | FPMH recurrent (3) | recurrent | `CourtClusterGenerator` (specified) | S8.6 |
| `SYN-TYP-012` | Stepped dwelling bands with exposed terraces | new | FPMH recurrent (2) | recurrent | `SteppedBandGenerator` (specified, plans per storey, D-095) | S8.7 |
| `SYN-TYP-013` | Incremental row with reserved expansion bays | new | FPMH provisional (1) | provisional | none: set aside (D-096) | — |
| `SYN-TYP-014` | Branching public mall between retail wings | new | MH recurrent (2) | recurrent | `BranchingMallGenerator` (specified) | S8.5 |
| `SYN-TYP-015` | Operating suite with separated clean and dirty routes | new | AD provisional (1) | provisional | `OperatingSuiteGenerator` (specified) | S8.6 |
| `SYN-TYP-016` | Cafeteria service edge beside a dining field | new | MH recurrent (2) | recurrent | `CafeteriaGenerator` (*Cafeteria*) | S8.4 |
| `SYN-TYP-017` | Central service core within an office plate | new | AD recurrent (2) | recurrent | `OfficePlateGenerator` (*Office Plate*) | S8.4 |
| `SYN-TYP-018` | Communal hub serving elder-care bedroom groups | new | MH recurrent (2) | recurrent | `CareHubGenerator` (specified) | S8.5 |
| `SYN-TYP-019` | Public foyer branching to distinct activity spaces | new | AD recurrent (2), MH provisional (1) | recurrent | `FoyerHallsGenerator` (specified) | S8.5 |
Counts are distinct named projects, not drawings. "Carried" means the family keeps the organisation of its `HDHM-TYP-*` baseline and gains evidence from the other books; "revised" means the same, with further named examples (`002`, `007`); the nine existing generators therefore stay valid for `001` to `009` (D-093). Generator names marked *specified* are fixed in ADR-006 and built in the sub-stage shown.
## Families carried from *High-Density Housing* (`001`–`009`)
Their relationships and rules are those of the baseline families, paraphrased in [hdhm-typologies.md](hdhm-typologies.md) under the `HDHM-TYP-*` IDs; the rule numbering is the same (`SYN-TYP-001-R001` corresponds to `HDHM-TYP-001-R001`, and so on). What the four-source set adds:
- **`SYN-TYP-001` Repeated stair bays in a linear bar.** One linear tract of repeated dwelling groups (`R001`), each with a local stair (`R002`), access distributed among the groups instead of one continuous shared route (`R003`); `R004` constraint, `R005` failure condition. The three cores seen in one project are an observation, not a bound. A linear repeated-bay case of the second book has an unconfirmed stair arrangement and is not counted.
- **`SYN-TYP-002` Continuous external gallery beside a linear bar.** Dwelling modules in one linear tract (`R001`), served by a continuous external gallery (`R002`) along which the shared route runs (`R003`); `R004`, `R005`. Evidence now from three books. Gallery side, end cores, and galleries on selected levels only vary among projects; the continuous access relation defines the family.
- **`SYN-TYP-003` Projecting wings joined by a common band.** Wings attached to a shared band (`R001`), the relation repeated with no fixed count or shape (`R002`); `R003`, `R004`. Wings may taper or shift.
- **`SYN-TYP-004` Open U-shaped court with stair-served wings.** Residential sides on three sides of a shared space (`R001`), the fourth side open (`R002`), stair access (`R003`); `R004`, `R005`. A two-dwelling U without repeated wings is a boundary case.
- **`SYN-TYP-005` Enclosed courtyard residential block.** Residential edges around one interior court (`R001`) that is enclosed rather than open-sided (`R002`); `R003`, `R004`. One project in each housing book; combined support in the corpus. Stair and gallery access appear as variants of one project.
- **`SYN-TYP-006` Attached narrow terrace row.** Narrow attached house plots in one linear row (`R001`), repeated (`R002`), with a shared dividing wall between neighbours (`R003`); `R004`, `R005`. Floors differ within a plot; slope and row alignment vary.
- **`SYN-TYP-007` Compact point-access tower plate.** A compact dwelling plate rather than a long bar (`R001`) with access gathered at a point (`R002`); `R003`, `R004`. Further named examples from *Architects' Data*; dwelling counts and arrangements vary; a branching radial plate (`010`) and a corridor plate test its boundary.
- **`SYN-TYP-008` One stair between a dwelling pair.** One compact linear plate for one pair (`R001`), the two dwellings on opposite sides of one shared stair (`R002`) that serves both (`R003`); `R004`, `R005`. One project; stacked apartments do not establish a lateral pair.
- **`SYN-TYP-009` Double-sided internal corridor bar.** A long bar with occupiable sides flanking an internal strip (`R001`), a shared linear route inside it (`R002`), dwellings or rooms on both sides (`R003`); `R004`, `R005`. Combined support from one project in each housing book.
## New housing families (`010`–`013`)
### `SYN-TYP-010` Radial lobes around a point tower core
- **Support:** provisional in FPMH and in the corpus (one project). **Inputs:** footprint form (branching), access system (point access), circulation topology (radial).
- **Relationships.** Dwelling lobes branch off a compact centre, their number and size left open (`R001`); every lobe is reached from that point-access centre (`R002`); movement spreads radially from the centre into the lobes (`R003`). `R004` keeps these relationships when other properties vary; `R005` rejects an organisation missing one.
- **Not supported by the source:** the number, size, and angle of the lobes, the dwellings per lobe, and every dimension. A compact plate without lobes is `007`.
### `SYN-TYP-011` Linked housing clusters around multiple courts
- **Support:** recurrent in FPMH (three projects) and in the corpus. **Inputs:** footprint form (a non-linear cluster), courtyard form (several shared courts), circulation topology (branching or distributed).
- **Relationships.** Dwelling modules aggregate into a non-linear, linked cluster (`R001`); several shared courts lie between dwelling groups within it (`R002`); a branching or distributed route connects the groups (`R003`). `R004`, `R005` as above.
- **Not supported by the source:** court shapes, the route hierarchy (which differ among the projects), the number of courts, and dimensions. One enclosed court is `005`.
### `SYN-TYP-012` Stepped dwelling bands with exposed terraces
- **Support:** recurrent in FPMH (two projects) and in the corpus. **Inputs:** footprint form (a band), another spatial feature (stepping terraces), access system.
- **Relationships.** Dwelling bands step back from storey to storey, so that the roofs of lower storeys become terraces, without a fixed setback (`R001`); the terraced dwellings are reached from a shared circulation edge (`R002`). `R003` constraint, `R004` failure condition.
- **Not supported by the source:** setback dimensions, the number of storeys, whether access is by stairs or an internal gallery (both occur), and dimensions. The plan differs by storey, so its generator needs plans per storey (D-095).
### `SYN-TYP-013` Incremental row with reserved expansion bays
- **Support:** provisional in FPMH and in the corpus (one project). **Inputs:** footprint form (a row), repetition, another spatial feature (reserved bays).
- **Relationships.** Built row-house bays alternate with bays reserved for infill (`R001`), and a household may later grow into the neighbouring reserved bay (`R002`). `R003`, `R004`.
- **BEMGen:** set aside, no generator (D-096): modelling the reserved bays needs a choice the precedent does not settle (outdoor gaps, built bays, unconditioned shells, or the growth state as an input), and the family rests on one provisional project.
## Non-residential families (`014`–`019`)
### `SYN-TYP-014` Branching public mall between retail wings
- **Support:** recurrent in MH (two projects) and in the corpus. **Inputs:** footprint form, access system, circulation topology (branching).
- **Relationships.** Retail wings and large destinations (anchors) lie around a branching public mall (`R001`), which joins the wings as one connected branching route along which shops and anchors are disposed (`R002`). `R003` keeps these relationships; `R004` rejects a floor organisation missing one.
- **Not supported by the source:** the size of wings and anchors, the angles and widths of the mall, the number of branches, and every dimension.
### `SYN-TYP-015` Operating suite with separated clean and dirty routes
- **Support:** provisional in AD and in the corpus (one project). **Inputs:** footprint form, access system, circulation topology (separated routes).
- **Relationships.** Operating theatres form repeated banks beside the support rooms (`R001`), and the banks are reached by distinct clean and dirty routes (`R002`). `R003`, `R004` as above.
- **Not supported by the source:** the number and size of theatres and banks, the support-room programme, and every dimension. A surgical floor whose theatres share one corridor, without documented separate routes, is outside the family.
### `SYN-TYP-016` Cafeteria service edge beside a dining field
- **Support:** recurrent in MH (two projects) and in the corpus. **Inputs:** footprint form, access system, circulation topology.
- **Relationships.** Food preparation and servery spaces lie along an edge of a larger dining field (`R001`); customers move past that service edge into the dining space (`R002`). `R003`, `R004` as above.
- **Not supported by the source:** counter count, seating capacity, aisle widths, the kitchen's internal layout, and every dimension.
### `SYN-TYP-017` Central service core within an office plate
- **Support:** recurrent in AD (two projects) and in the corpus. **Inputs:** footprint form (a plate), access system (a central core), circulation topology.
- **Relationships.** Office work areas gather around a central service core (`R001`), which holds the lifts and shared services (`R002`); the plate holds several work areas around the one shared core. `R003`, `R004` as above.
- **Not supported by the source:** the core's shape and position within the centre, the furniture layout, and every dimension. An office floor whose core lies near an edge is outside the family.
### `SYN-TYP-018` Communal hub serving elder-care bedroom groups
- **Support:** recurrent in MH (two projects) and in the corpus. **Inputs:** footprint form, access system, circulation topology (branching).
- **Relationships.** Bedroom groups are arranged around a common communal and service centre (`R001`), and circulation branches from that centre to the groups (`R002`). `R003`, `R004` as above.
- **Not supported by the source:** the number of bedrooms, the wing lengths, care-room sizes, and every dimension.
### `SYN-TYP-019` Public foyer branching to distinct activity spaces
- **Support:** recurrent in AD (two projects), provisional in MH (one), recurrent in the corpus. **Inputs:** footprint form, access system, circulation topology (branching).
- **Relationships.** Distinct activity spaces lie around a common public foyer, which also leads to the support rooms (`R001`), and access branches from the foyer to those spaces (`R002`). `R003`, `R004` as above.
- **Not supported by the source:** the program mix (sport or community uses occur), hall dimensions, and the number of halls. A hall added beside an existing space through a service zone is outside the family.
## Plans outside every family
The source keeps six named plans in its catalogue without a family (an edge-core office, a hospital floor with a shared corridor, an emergency department organised by its entrances, two museums with galleries around courts or along a spine, and a church hall reached through a service zone), and it excludes a schematic whose floor extent is not established. BEMGen builds nothing for them.
## Related documents
- [*High-Density Housing* families](hdhm-typologies.md): the issued baseline of `001`–`009`.
- [ADR-006 Plan generation mechanism](../../decisions/ADR-006-plan-generation-mechanism.md): parameter records, defaults, zone IDs, and how each generator realises its family.
- [ADR-014 Program types and department zoning](../../decisions/ADR-014-program-types-and-department-zoning.md): the non-residential space types, their presets, and which rooms form one zone.
- [Decision log](../../decisions/decision-log.md): D-093 (source), D-094 (non-residential scope), D-095 (plans per storey), D-096 (`013` set aside), D-097 (department zoning), D-098 (sub-stages).
- [x] Step 3: Edit
docs/research/precedents/hdhm-typologies.md
Make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
# *High-Density Housing* typology families
with
# *High-Density Housing* typology families
> **Issued baseline.** Since D-093 the precedent set is the four-source family set `SYN-TYP-001` to `SYN-TYP-019`, paraphrased in [syn-typologies.md](syn-typologies.md). The nine families below are carried there as `SYN-TYP-001` to `SYN-TYP-009` with the same organisation and rule numbering; this page stays as the issued baseline of S8.1–S8.3 and is not updated further.
- [x] Step 4: Check and commit
git status --short --untracked-files=all
git add docs/research/precedents/syn-typologies.md docs/research/precedents/hdhm-typologies.md
git grep -c -e "^| \`SYN-TYP-0" -- docs/research/precedents/syn-typologies.md
git grep -c "syn-typologies.md" -- docs/research/precedents/hdhm-typologies.md
git commit -m "docs(research): paraphrase the four-source typology families"
Expected: git status lists M docs/research/precedents/hdhm-typologies.md and ?? docs/research/precedents/syn-typologies.md; then docs/research/precedents/syn-typologies.md:19 (the 19 rows of the overview table) from the first command and docs/research/precedents/hdhm-typologies.md:1 from the second (git grep searches tracked files, so it runs after git add). The page links to ADR-014, which Task 2 creates; the two documents link to each other, so this link resolves from Task 2 on.
Task 2: ADR-014 program types and department zoning¶
Files:
- Create: docs/decisions/ADR-014-program-types-and-department-zoning.md
Interfaces:
- Consumes: D-023, D-061, D-065, D-094, D-097, D-098; the precedent page (Task 1); SpaceType (S1), PresetValues and ExampleResidentialPresets (S4.1), LoadType.Ventilation and LoadBasis.AirChangesPerHour (S1).
- Produces: the decision that Tasks 5, 6, 7, and 8 implement and S8.5–S8.7 follow: twelve new space types appended after Mixed (with why OperatingTheatre, CleanCorridor, DirtyCorridor, CareBedroom, and CareCommunal are named so) and Core, Lobby, and Service reused; the fifteen illustrative presets of ExampleNonResidentialPresets with every value, its reason, and the shape of each schedule; one default preset component per type in 1 Program; and, for every new family (010, 011, 012 as housing; 014–019), the rooms that form each department zone and its space type. Status: provisional, accepted by the controller under D-098, pending the owner's reading.
- [x] Step 1: Create
docs/decisions/ADR-014-program-types-and-department-zoning.md(full file)
# ADR-014: Program types and department zoning
Status: Accepted (by the controller under D-098; provisional pending the owner's reading)
Date: 2026-10-02
Decisions: D-009, D-023, D-024, D-026, D-038, D-047, D-061, D-065, D-085, D-093, D-094, D-096, D-097, D-098; applies [ADR-005](ADR-005-conditioning-and-setpoints.md), [ADR-006](ADR-006-plan-generation-mechanism.md), and [ADR-007](ADR-007-load-basis-aggregation.md)
## Context
Through S8.3 every plan generator placed housing: dwelling units, stairs, and corridors, with the illustrative presets `ExampleResidentialPresets` (D-023, D-061). The four-source precedent set (D-093, [syn-typologies.md](../research/precedents/syn-typologies.md)) adds non-residential families, and D-094 puts every program type of the set in scope: retail, an operating suite, food service, office, elder care, and assembly or community uses. Each new space type needs its own typed program preset with explicit, documented assumptions (D-094), and D-097 zones a non-residential floor at its most detailed level per department: a contiguous group of rooms of one program in one part of the floor, not one zone per room or tenancy. This ADR fixes the new space types, one illustrative preset per type, and, for every new family, which rooms form one zone and which space type that zone has.
## Options considered
### Space types
1. **One generic `NonResidential` type with a program name.** Few enum values, but the program would be carried by a name, which D-061 and AGENTS.md forbid (semantics are explicit data, never encoded in names).
2. **Reuse `Other` with different presets.** A generator could not say which preset each zone takes: one slot per space type (D-061) needs distinct types.
3. **One space type per department program that a family places (chosen).** Explicit data, one typed preset slot per type, and the existing default-preset pattern (D-061, D-065) extends unchanged. Existing types are reused where they mean the same thing.
### Zoning of non-residential floors (settled by D-097)
Rooms or tenancies as zones would need a room layout the precedents do not give and would break the parity with housing, where a dwelling is never subdivided (D-009). One zone per department is the most detailed level; the simplifiers and aggregators coarsen it as for housing.
## Decision
### Space types
`SpaceType` gets twelve values, appended after the existing ones so that existing values keep their order: `Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, `ActivityHall`. Three existing values gain a non-residential use without changing meaning: `Core` (an office service core: lifts, stairs, toilets, risers), `Lobby` (a public foyer), and `Service` (the support rooms of a foyer family: changing rooms, stores, toilets).
| Space type | Department it zones | Why its own type |
| --- | --- | --- |
| `Office` | an office work area | the work-area program of `017` |
| `Core` (reused) | an office service core | already "service or circulation core"; distinct from `Stair`, which stays the housing core |
| `Retail` | the shops of one mall wing, or one anchor store | shop program of `014`; anchors are shops with the same program |
| `Mall` | the public mall route | a conditioned public route with shop-hour occupancy, unlike a residential `Corridor` |
| `Kitchen` | food preparation and servery as one department | high equipment and extract ventilation (`016`) |
| `Dining` | the dining field | dense lunchtime occupancy (`016`) |
| `OperatingTheatre` | one bank of operating theatres | high equipment and ventilation (`015`) |
| `CleanCorridor` | the clean route of an operating suite | the separation of routes is the family's defining relation (`015-R002`), so the two routes are distinct data |
| `DirtyCorridor` | the dirty route | as above |
| `ClinicalSupport` | the support rooms of an operating suite | scrub, anaesthesia, stores, recovery, sluice (`015`) |
| `CareBedroom` | one group of care bedrooms | 24-hour occupied, warmer setpoints (`018`) |
| `CareCommunal` | the communal and service hub of a care home | day-long communal use with meal peaks (`018`) |
| `ActivityHall` | one activity hall | evening and weekend use, lower heating setpoint (`019`) |
| `Lobby` (reused) | a public foyer | already "entrance lobby" |
| `Service` (reused) | the support rooms of a foyer family | already "service space" |
`Mechanical`, `Other`, and `Mixed` are unchanged; `Mixed` remains the type of a simplified zone that combines several types. Names were checked against shorter alternatives: `OperatingTheatre` rather than `Surgery` (which also names a clinic), `CareBedroom` and `CareCommunal` rather than `Bedroom` (a room of a dwelling, never modelled, D-009).
### Presets: `ExampleNonResidentialPresets`
A new static class in `Lod.Core.Programs`, following `ExampleResidentialPresets`: for each of the fifteen types above a `…Values` (`PresetValues`) and the `ProgramPreset` built from it, named `Example <Type>` (`Example Office`, `Example Operating Theatre`, …). **The values are illustrative round numbers chosen to be plausible for development and tests (D-023). They are not taken from the DOE prototype buildings, from any standard, or from measured data**, and they are not research inputs; sourced presets remain an open research input ([program-presets.md](../research/program-presets.md) §3). Every preset is conditioned: each space is one that people occupy or pass through and that is normally heated (the residential stair is the only unconditioned example). The year starts on a Monday, as for the residential presets.
| Preset | WWR | Heating / cooling °C | Occupancy, people/m² | Lighting, W/m² | Equipment, W/m² | Ventilation, 1/h | Infiltration, 1/h |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Office | 0.4 | 21 / 24 | 0.10 office | 10 office lighting | 10 office equipment | — | 0.3 always on |
| Core | 0.0 | 21 / 24 | — | 5 office lighting | 3 office equipment | — | 0.3 always on |
| Retail | 0.3 | 20 / 24 | 0.20 retail | 15 retail lighting | 5 retail equipment | — | 0.5 always on |
| Mall | 0.2 | 20 / 24 | 0.10 retail | 10 retail lighting | — | — | 0.8 always on |
| Kitchen | 0.1 | 18 / 26 | 0.05 kitchen staff | 12 kitchen hours | 30 electric (kitchen electric), 40 gas (kitchen gas) | 15 kitchen hours | 0.3 always on |
| Dining | 0.4 | 21 / 24 | 0.60 dining | 10 dining hours | 2 dining hours | — | 0.3 always on |
| Operating theatre | 0.0 | 20 / 22 | 0.10 theatre sessions | 25 theatre services | 30 theatre services | 20 theatre ventilation | 0.1 always on |
| Clean corridor | 0.1 | 20 / 24 | — | 8 always on | — | — | 0.3 always on |
| Dirty corridor | 0.1 | 20 / 24 | — | 8 always on | — | — | 0.3 always on |
| Clinical support | 0.2 | 21 / 24 | 0.05 theatre sessions | 12 theatre services | 10 theatre services | — | 0.3 always on |
| Care bedroom | 0.3 | 22 / 25 | 0.04 care bedroom | 6 care bedroom lighting | 3 care bedroom equipment | — | 0.3 always on |
| Care communal | 0.4 | 22 / 25 | 0.15 care communal | 10 care communal hours | 5 care communal hours | — | 0.3 always on |
| Lobby | 0.5 | 20 / 24 | 0.10 community | 10 community hours | — | — | 0.6 always on |
| Activity hall | 0.2 | 18 / 25 | 0.10 community | 12 community hours | 2 community hours | — | 0.3 always on |
| Service | 0.1 | 18 / 26 | — | 6 community hours | — | — | 0.3 always on |
Every load is expressed per floor area except ventilation and infiltration, which are air changes per hour (ADR-007; the magnitude is m³/h = value × zone volume). Equipment is `ElectricEquipment` except the kitchen's 40 W/m² of `GasEquipment`. Ventilation (`LoadType.Ventilation`) is given only where the program clearly needs mechanical air beyond infiltration: the kitchen's extract hoods and the operating theatres' air changes. No preset defines `DomesticHotWater`.
**Reasons for the values.**
- **Office.** A typical open-plan density (one person per 10 m²), lighting and small-power densities of an office floor, and the glazing of a curtain-walled plate. **Core:** no occupancy of its own (people pass through, as in the residential corridor); lifts and riser fans as small equipment; WWR 0 because a core is enclosed by shafts and service rooms, so a core on a façade gets no window by default.
- **Retail and mall.** Shop-floor densities with display lighting; the mall is less dense and has no equipment. Heating 20 °C because customers keep their coats on. Higher infiltration for frequently opened entrances, highest for the mall, whose ends are the building's entrances.
- **Kitchen and dining.** The kitchen carries the cooking load (gas ranges, electric ovens, refrigeration and dishwashing) and a hood extract of 15 air changes while it operates; a wide 18–26 °C band because cooking heat dominates. The dining field is densely occupied around lunch.
- **Operating suite.** Theatres hold a team of about six in 60 m², high equipment and lighting, a tight 20–22 °C band, 20 air changes during sessions with a setback to half at night and at weekends, and very low infiltration because theatres are kept at positive pressure; WWR 0 because theatres are usually windowless. The clean and dirty corridors have the same values: their difference is the route, not the program, and two types keep the routes apart as data. Support rooms have a light, session-hour occupancy.
- **Elder care.** Residents are older and less active, so both setpoints are 1 °C warmer than for dwellings. Bedrooms are occupied at night and partly by day, every day; the communal hub is busy all day with meal peaks, and a care station keeps a little light and occupancy at night.
- **Community.** The foyer and halls are used for daytime classes and mostly in the evenings and at weekends; a sports hall is heated to 18 °C, and the foyer is glazed (WWR 0.5) with an entrance's infiltration. Support rooms (changing rooms, stores, toilets) are heated to a wide 18–26 °C band and have no occupancy of their own.
**Schedules.** Fractions per hour of the day (hour 0 is the first hour); Monday to Friday use the weekday profile, Saturday and Sunday the weekend profile, except the retail schedules, which have their own Saturday and Sunday profiles because shops trade longest on Saturday.
| Schedule (`Example …`) | Weekday | Weekend |
| --- | --- | --- |
| `Office Occupancy` | 0 until 7:00; 0.1, 0.5 (7–9); 0.95 (9–12); 0.5 (12–13); 0.95 (13–17); 0.5, 0.2, 0.1 (17–20); then 0 | 0 |
| `Office Lighting` | 0.05 until 7:00; 0.3, 0.8; 0.9 (9–12); 0.8; 0.9 (13–17); 0.6, 0.3, 0.2; then 0.05 | 0.05 |
| `Office Equipment` | 0.2 until 7:00; 0.4, 0.7; 0.9 (9–12); 0.7; 0.9 (13–17); 0.6, 0.4, 0.3; then 0.2 | 0.2 |
| `Retail Occupancy` | 0 until 9:00; 0.3, 0.5, 0.6, 0.8, 0.8, 0.6, 0.6, 0.7, 0.8, 0.7, 0.4 (9–20); then 0 | Saturday 0 until 9:00; 0.5, 0.8, 1.0 (11–16), 0.9, 0.8, 0.6, 0.3 (9–20); Sunday 0 until 11:00; 0.4, 0.6, 0.7, 0.7, 0.6, 0.4 (11–17); then 0 |
| `Retail Lighting` | 0.1 until 8:00; 0.5; 1.0 (9–20); 0.5; then 0.1 | Saturday as the weekday; Sunday 0.1 until 10:00; 0.5; 1.0 (11–17); 0.5; then 0.1 |
| `Retail Equipment` | 0.3 until 8:00; 0.6; 0.9 (9–20); 0.6; then 0.3 | Saturday as the weekday; Sunday 0.3 until 10:00; 0.6; 0.9 (11–17); 0.6; then 0.3 |
| `Kitchen Staff` | 0 until 6:00; 0.3, 0.8; 1.0 (8–15); 0.5; then 0 | 0 |
| `Kitchen Hours` | 1.0 from 6:00 to 16:00, else 0 | 0 |
| `Kitchen Electric` | 0.2 until 6:00; 0.4; 0.6 (7–11); 0.9, 1.0, 0.9 (11–14); 0.6, 0.4; then 0.2 | 0.2 |
| `Kitchen Gas` | 0 until 7:00; 0.3 (7–10); 0.6, 1.0, 1.0, 0.6, 0.2 (10–15); then 0 | 0 |
| `Dining Occupancy` | 0 until 7:00; 0.1, 0.2, 0.1, 0.1 (7–11); 0.4, 1.0, 0.8, 0.3, 0.1 (11–16); then 0 | 0 |
| `Dining Hours` | 1.0 from 7:00 to 16:00, else 0 | 0 |
| `Theatre Sessions` | 0 until 7:00; 0.5; 1.0 (8–18); 0.5; then 0 | 0 |
| `Theatre Services` | 0.2 until 7:00; 0.6; 1.0 (8–18); 0.6; then 0.2 | 0.2 |
| `Theatre Ventilation` | 0.5 until 7:00; 1.0 (7–19); then 0.5 | 0.5 |
| `Care Bedroom Occupancy` (every day) | 1.0 until 7:00; 0.8, 0.5; 0.3 (9–12); 0.2; 0.4 (13–16); 0.3, 0.2, 0.4, 0.6, 0.8 (16–21); then 1.0 | as the weekday |
| `Care Bedroom Lighting` (every day) | 0.05 until 6:00; 0.3, 0.6, 0.4; 0.1 (9–16); 0.2, 0.4, 0.6, 0.8, 0.8, 0.6, 0.3, 0.1 (16–24) | as the weekday |
| `Care Bedroom Equipment` | constant 0.5 | constant 0.5 |
| `Care Communal Occupancy` (every day) | 0.05 until 7:00; 0.3, 0.9, 0.6, 0.7, 0.7, 1.0, 0.6, 0.7, 0.7, 0.7, 1.0, 0.8, 0.6, 0.4, 0.2 (7–22); then 0.05 | as the weekday |
| `Care Communal Hours` (every day) | 0.2 until 7:00; 1.0 (7–22); then 0.2 | as the weekday |
| `Community Occupancy` | 0 until 9:00; 0.2 (9–17); 0.6; 0.9 (18–21); 0.6, 0.2; then 0 | 0 until 9:00; 0.8 (9–20); 0.4; then 0 |
| `Community Hours` | 1.0 from 8:00 to 23:00, else 0 | 1.0 from 8:00 to 21:00, else 0 |
| `Always On` | constant 1.0 (the residential presets' schedule) | constant 1.0 |
The exact hourly values are in `ExampleNonResidentialPresets` and listed in [program-presets.md](../research/program-presets.md) §2.2.
**Default preset components (D-061, D-065).** One per type in panel *1 Program*, each visible on the toolbar and built on `DefaultPresetComponent`, with these values as input defaults and the remark that they are illustrative: *Office Preset*, *Core Preset*, *Retail Preset*, *Mall Preset*, *Kitchen Preset*, *Dining Preset*, *Operating Theatre Preset*, *Clean Corridor Preset*, *Dirty Corridor Preset*, *Clinical Support Preset*, *Care Bedroom Preset*, *Care Communal Preset*, *Lobby Preset*, *Activity Hall Preset*, and *Service Preset*. The existing label and unit mapping of `DefaultPresetComponent` already covers every load above (*Gas Equipment* in W/m², *Ventilation* and *Infiltration* in 1/h); no component is special-cased.
### Department zoning per family (D-097)
At the most detailed level each family below places these zones. "One zone" means one polygon that may surround other zones (a ring) but is never split into rooms or tenancies. Housing families keep one zone per dwelling and storey (D-009, D-085), and their stairs and corridors are their own zones.
| Family | Zones and their space types | Not zoned separately |
| --- | --- | --- |
| `SYN-TYP-010` Radial lobes (housing) | the point-access core (`Stair`); each dwelling of each lobe (`DwellingUnit`), one zone per dwelling and storey | rooms of a dwelling |
| `SYN-TYP-011` Court cluster (housing) | each stair (`Stair`) and each dwelling (`DwellingUnit`) of every wing | rooms; the courts, which are outside the building |
| `SYN-TYP-012` Stepped bands (housing) | the stair (`Stair`), the shared circulation edge (`Corridor`), each dwelling (`DwellingUnit`), per storey | rooms; the terraces, which are outdoor roofs |
| `SYN-TYP-014` Branching mall | the public mall, hub and branches together (`Mall`); the shops on each side of each branch, one zone per side (`Retail`); each anchor store (`Retail`) | individual shops and tenancies |
| `SYN-TYP-015` Operating suite | each bank of operating theatres (`OperatingTheatre`); each clean corridor (`CleanCorridor`); each dirty corridor (`DirtyCorridor`); the support rooms together (`ClinicalSupport`) | individual theatres, scrub and anaesthetic rooms, stores, recovery, sluice |
| `SYN-TYP-016` Cafeteria | the kitchen and servery as one department (`Kitchen`); the dining field (`Dining`) | the servery counter, stores, wash-up, queue |
| `SYN-TYP-017` Office plate | the office work area, one ring around the core (`Office`); the service core (`Core`) | individual work areas, meeting rooms, lifts, stairs, toilets |
| `SYN-TYP-018` Care hub | the communal and service hub (`CareCommunal`); the bedrooms on each side of each wing corridor, one zone per side (`CareBedroom`); each wing corridor (`Corridor`) | individual bedrooms and en-suites, the care station |
| `SYN-TYP-019` Foyer and halls | the foyer (`Lobby`); each activity hall (`ActivityHall`); the support rooms together (`Service`) | individual changing rooms, stores, toilets |
`SYN-TYP-013` has no generator (D-096). The `001`–`009` families are unchanged (ADR-006).
## Consequences
- The enum grows by twelve values at its end; existing values, snapshots, and diagnostics are unchanged. *Semantic Merge* groups by space type as before, so a non-residential floor merges departments of the same type that touch; *Perimeter Core* and the single-zone methods give `Mixed` zones as before.
- Fifteen illustrative presets and fifteen default preset components; `ExampleResidentialPresets` is unchanged. Tests check that every preset builds, its space type, conditioning, setpoints, WWR, and the design value and annual fraction of every load.
- Non-residential zones are departments, so the detailed model has fewer and larger zones than a room-level model would; comparisons with room-level reference models must state this.
- The presets are not representative of any building or standard; results computed with them are development results only (D-023).
- Changing a value changes the snapshots of the families that use it and is recorded in this ADR and in [program-presets.md](../research/program-presets.md).
- [x] Step 2: Check and commit
git grep -c "ADR-014-program-types-and-department-zoning.md" -- docs/research/precedents/syn-typologies.md
git status --short --untracked-files=all
git add docs/decisions/ADR-014-program-types-and-department-zoning.md
git commit -m "docs(decisions): add adr-014 on program types and department zoning"
Expected: docs/research/precedents/syn-typologies.md:2 (the precedent page's two links now resolve); git status lists only ?? docs/decisions/ADR-014-program-types-and-department-zoning.md.
Task 3: The study covers typologies of every program type (D-094)¶
Files:
- Modify: AGENTS.md, README.md (last changed in S8.3), docs/LOD_geometric_zoning_equivalence_research_brief.md (last changed in S8.1), docs/LOD_grasshopper_plugin_repository_spec.md (unchanged since S0), docs/plans/2026-09-30-implementation-roadmap.md (last changed by 436e415), and the header line of docs/plans/2026-10-02-s8.1-bar-typologies.md, docs/plans/2026-10-02-s8.2-non-rectangular-typologies.md, and docs/plans/2026-10-02-s8.3-enclosed-courtyard.md
Interfaces:
- Consumes: D-093 to D-098; the precedent page (Task 1); ADR-014 (Task 2).
- Produces: research question 2 and §4 of the brief widened from residential to building typologies of every program type, departments in §3 and in the Z0 level, phase 3 and deliverable 1 widened; the repository spec's §1, the new space types in §7, and milestone 6; the README's Z0 line and scope line; one AGENTS.md paragraph before "Not implemented yet" (the precedent set, D-096, D-097, D-098); the roadmap's goal, Z0 constraint, the S8.4–S8.7 rows with the tags v0.8.3–v0.8.6, S6 after S8.7, S9 after S6 and S8.7, the linear diagram, the S8 section and its heading ### S8 — Typologies (S8.1–S8.7, D-083, D-084, D-098), the layout lines of §2, and the revision note v9. GLOBAL.md and the roadmap's Progress lines are not touched. The three S8.1–S8.3 plans link to the roadmap's S8 heading, whose anchor changes with it, so Step 6 updates their links (note 3).
- [x] Step 1: Edit
AGENTS.md
Make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
Checkpoint 1 (D-030) is reached: stages S0–S4 of the [implementation roadmap](docs/plans/2026-09-30-implementation-roadmap.md) are complete (tag `v0.4.0`), and so are the revision stages S4.1 (tag `v0.4.1`, D-064) and S4.2 (tag `v0.4.2`, D-073), stage S5 (tag `v0.5.0`, D-079), and sub-stages S8.1 (tag `v0.8.0`), S8.2 (tag `v0.8.1`), and S8.3 (tag `v0.8.2`, D-084). The pipeline runs end to end for nine plan generators: program presets → a plan generator (one `PlanGenerator` subclass per *High-Density Housing* typology family with typed parameter and preset records, [ADR-006](docs/decisions/ADR-006-plan-generation-mechanism.md): `LinearPlanGenerator`, the double-sided internal corridor bar `HDHM-TYP-009`; the bar families `StairBayBarGenerator` 001, `StairPairGenerator` 008, `GalleryBarGenerator` 002 with its gallery not modelled, and `TerraceRowGenerator` 006 with one dwelling zone per plot and storey, D-085; the non-rectangular families `PointPlateGenerator` 007, a square plate whose core has no outdoor wall, `WingedBandGenerator` 003, a comb, and `OpenCourtGenerator` 004, a U; and `EnclosedCourtGenerator` 005, four wings around a court closed on all four sides, the first footprint with a hole; one typed preset per space type, D-061; optionally rotated and moved by `PlanTransform`, D-070) → plan simplifiers (`NoSimplification`, `SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`) → floor aggregators (`Stack`, `StackedFloorZoneMultiplier`, `SingleZonePerFloorType`, `SingleZoneBuilding`, D-071; floors and ceilings designated on the assembled stack, D-068) → validation → `Convert2BEM` (heat transfer between zones an export option, off by default, D-069), as described in [docs/architecture/pipeline.md](docs/architecture/pipeline.md). Every generator places one centred window per outdoor wall (D-026), except on a wall shorter than 1 m or where the window would be narrower or lower than 0.3 m (`GeometryLimits`, info `WindowOmitted`, [ADR-011](docs/decisions/ADR-011-minimum-wall-and-window-geometry.md)). Wherever zones are merged and walls are rebuilt (`SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`, `SingleZonePerFloorType`, `SingleZoneBuilding`), every outdoor wall carries one centred window with the glazed area of the source windows it covers (`CentredWindows`, D-079, [ADR-012](docs/decisions/ADR-012-centred-windows.md)); the other steps keep their input's windows. Merged zones are unions of their sources as tiles that keep the sources' own vertices, so they may have holes and stay exact on rotated plans, and vertices on edges are recognised at their original positions (D-088, [ADR-002](docs/decisions/ADR-002-geometry.md)). Every step supports footprints with holes: a court is outside the building, its edges are outdoor walls facing into it, every footprint edge, court edges included, must be covered by outdoor zone edges (ADR-002), and `PerimeterCore` gives the court façades their own zones `P-Court-<Bin>` around a core that is a ring ([ADR-008](docs/decisions/ADR-008-perimeter-core-corners.md)); `PerimeterCore` needs every part of a footprint, including the wings between the outer and the court façades, to be wider than twice its depth and otherwise fails with `PlanTooNarrow` (D-087). Vertical aggregation follows [ADR-013](docs/decisions/ADR-013-vertical-aggregation-methods.md), which supersedes [ADR-009](docs/decisions/ADR-009-vertical-aggregation.md) where it differs. In *Convert2BEM* and the previews, zone solids are outward-oriented and every surface and window faces out of its zone (D-091). ADR-006, ADR-011, ADR-012, ADR-013, and the S4.2, S5, S8.1, S8.2, and S8.3 plans were accepted by the controller and await the owner's reading (D-075, D-080, D-084). In Grasshopper every component has an icon drawn by `scripts/make_icons.py` (D-063), and the generator, *Transform Plan*, simplifier, and aggregator components have a *Preview Location* input that moves only their preview (D-062). Until the owner says otherwise, changes need not keep backward compatibility, but component GUIDs never change (D-064).
with
Checkpoint 1 (D-030) is reached: stages S0–S4 of the [implementation roadmap](docs/plans/2026-09-30-implementation-roadmap.md) are complete (tag `v0.4.0`), and so are the revision stages S4.1 (tag `v0.4.1`, D-064) and S4.2 (tag `v0.4.2`, D-073), stage S5 (tag `v0.5.0`, D-079), and sub-stages S8.1 (tag `v0.8.0`), S8.2 (tag `v0.8.1`), and S8.3 (tag `v0.8.2`, D-084). The pipeline runs end to end for nine plan generators: program presets → a plan generator (one `PlanGenerator` subclass per *High-Density Housing* typology family with typed parameter and preset records, [ADR-006](docs/decisions/ADR-006-plan-generation-mechanism.md): `LinearPlanGenerator`, the double-sided internal corridor bar `HDHM-TYP-009`; the bar families `StairBayBarGenerator` 001, `StairPairGenerator` 008, `GalleryBarGenerator` 002 with its gallery not modelled, and `TerraceRowGenerator` 006 with one dwelling zone per plot and storey, D-085; the non-rectangular families `PointPlateGenerator` 007, a square plate whose core has no outdoor wall, `WingedBandGenerator` 003, a comb, and `OpenCourtGenerator` 004, a U; and `EnclosedCourtGenerator` 005, four wings around a court closed on all four sides, the first footprint with a hole; one typed preset per space type, D-061; optionally rotated and moved by `PlanTransform`, D-070) → plan simplifiers (`NoSimplification`, `SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`) → floor aggregators (`Stack`, `StackedFloorZoneMultiplier`, `SingleZonePerFloorType`, `SingleZoneBuilding`, D-071; floors and ceilings designated on the assembled stack, D-068) → validation → `Convert2BEM` (heat transfer between zones an export option, off by default, D-069), as described in [docs/architecture/pipeline.md](docs/architecture/pipeline.md). Every generator places one centred window per outdoor wall (D-026), except on a wall shorter than 1 m or where the window would be narrower or lower than 0.3 m (`GeometryLimits`, info `WindowOmitted`, [ADR-011](docs/decisions/ADR-011-minimum-wall-and-window-geometry.md)). Wherever zones are merged and walls are rebuilt (`SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`, `SingleZonePerFloorType`, `SingleZoneBuilding`), every outdoor wall carries one centred window with the glazed area of the source windows it covers (`CentredWindows`, D-079, [ADR-012](docs/decisions/ADR-012-centred-windows.md)); the other steps keep their input's windows. Merged zones are unions of their sources as tiles that keep the sources' own vertices, so they may have holes and stay exact on rotated plans, and vertices on edges are recognised at their original positions (D-088, [ADR-002](docs/decisions/ADR-002-geometry.md)). Every step supports footprints with holes: a court is outside the building, its edges are outdoor walls facing into it, every footprint edge, court edges included, must be covered by outdoor zone edges (ADR-002), and `PerimeterCore` gives the court façades their own zones `P-Court-<Bin>` around a core that is a ring ([ADR-008](docs/decisions/ADR-008-perimeter-core-corners.md)); `PerimeterCore` needs every part of a footprint, including the wings between the outer and the court façades, to be wider than twice its depth and otherwise fails with `PlanTooNarrow` (D-087). Vertical aggregation follows [ADR-013](docs/decisions/ADR-013-vertical-aggregation-methods.md), which supersedes [ADR-009](docs/decisions/ADR-009-vertical-aggregation.md) where it differs. In *Convert2BEM* and the previews, zone solids are outward-oriented and every surface and window faces out of its zone (D-091). ADR-006, ADR-011, ADR-012, ADR-013, and the S4.2, S5, S8.1, S8.2, and S8.3 plans were accepted by the controller and await the owner's reading (D-075, D-080, D-084). In Grasshopper every component has an icon drawn by `scripts/make_icons.py` (D-063), and the generator, *Transform Plan*, simplifier, and aggregator components have a *Preview Location* input that moves only their preview (D-062). Until the owner says otherwise, changes need not keep backward compatibility, but component GUIDs never change (D-064).
The precedent set is the four-source family set `SYN-TYP-001` to `SYN-TYP-019` of every program type, housing and non-residential ([docs/research/precedents/syn-typologies.md](docs/research/precedents/syn-typologies.md), D-093, D-094); `SYN-TYP-013` is set aside (D-096), non-residential floors are zoned one zone per department (D-097, [ADR-014](docs/decisions/ADR-014-program-types-and-department-zoning.md)), and the nine new families are built in S8.4–S8.7 before S6 (D-098).
- [x] Step 2: Edit
README.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
→ PlanGenerator (LinearPlanGenerator, ...) → IGeneratedPlan detailed plan, one zone per dwelling unit (Z0)
with
→ PlanGenerator (LinearPlanGenerator, ...) → IGeneratedPlan detailed plan, one zone per dwelling unit or department (Z0)
- Replace
In scope: program presets, parametric plan generation, plan simplification, floor aggregation, equivalent load/schedule/setpoint calculation, window generation, invariant validation, and conversion to ClimateStudio-ready Grasshopper inputs (then IDF).
with
In scope: building typologies of every program type of the precedent set, housing and non-residential (D-094); program presets, parametric plan generation, plan simplification, floor aggregation, equivalent load/schedule/setpoint calculation, window generation, invariant validation, and conversion to ClimateStudio-ready Grasshopper inputs (then IDF).
- [x] Step 3: Edit
docs/LOD_geometric_zoning_equivalence_research_brief.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
2. How does the effect of zoning simplification vary by residential building typology?
with
2. How does the effect of zoning simplification vary by building typology, across housing and non-residential programs (D-094)?
- Replace
The reference representation may include:
- individual dwelling units,
with
The reference representation may include:
- individual dwelling units,
- departments of non-residential floors (D-097),
- Replace
## 4. Residential Building Typologies
with
## 4. Building Typologies
- Replace
The first study phase should focus on residential buildings.
with
The first study phase focused on residential buildings. Since D-094 the study covers every program type of its precedent set (D-093): housing, retail, healthcare (operating suites), food service, office, elder care, and assembly or community uses. The typologies are the families `SYN-TYP-001` to `SYN-TYP-019` of [docs/research/precedents/syn-typologies.md](research/precedents/syn-typologies.md), except `SYN-TYP-013` (D-096). At the most detailed level a dwelling is one zone per storey (D-009, D-085) and a non-residential floor has one zone per department: a contiguous group of rooms of one program in one part of the floor (D-097, ADR-014). The residential typologies of §4.1–§4.4 were the starting set.
- Replace
Examples:
- individual dwelling units,
with
Examples:
- individual dwelling units,
- departments of non-residential floors, such as an office work area, a kitchen and servery, or a bank of operating theatres (D-097),
- Replace
- additional residential typologies,
with
- additional typologies, residential and non-residential (D-094),
- Replace
1. parametric residential plan generators,
with
1. parametric plan generators for residential and non-residential typologies,
- [x] Step 4: Edit
docs/LOD_grasshopper_plugin_repository_spec.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
- parametric residential floor plan generation,
with
- parametric floor plan generation for residential and non-residential building typologies (D-094),
- Replace
- Other
with
- Other
Since D-094 the categories also cover non-residential programs: `Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, and `ActivityHall`, with `Core`, `Lobby`, and `Service` reused for office cores, public foyers, and support rooms ([ADR-014](decisions/ADR-014-program-types-and-department-zoning.md)). A non-residential floor is zoned per department, a dwelling per storey (D-097, D-085).
- Replace
- common generator abstractions.
with
- common generator abstractions,
- generators for the non-residential typologies of the precedent set (D-094).
- [x] Step 5: Edit
docs/plans/2026-09-30-implementation-roadmap.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
> **Revisions:** v1 (2026-09-30) initial draft · v2 (2026-09-30) pipeline-first restructure after D-015 to D-024: Grasshopper objects instead of JSON, the D-018 pipeline vocabulary, ClimateStudio output without simulation, precedent study moved after the pipeline works · v3 (2026-10-01) plan review: conditioning from presets (D-038), explicit windows and a separate window-transformation stage S5 with later stages renumbered (D-039), multiplier without multiplied ground or roof (D-040), enforced façade coverage (D-041); facts established while writing the S0–S4 plans · v4 (2026-10-01) plan review: representative storeys at their true elevations (D-045), exposed storeys of multiplied floor types split off automatically so floor types may differ in footprint (D-046, supersedes D-040), loads aggregated per load type and basis (D-047) · v5 (2026-10-01) Checkpoint 1 review: revision stage S4.1 before S5 (D-061 to D-064) · v6 (2026-10-01) S4.1 review: revision stage S4.2 before S5 (D-068 to D-077) · v7 (2026-10-01) windows of merged zones: one centred window per rebuilt wall with the wall's glazed area, window levels W1–W5 dropped, S5 renamed "Centred windows on rebuilt walls" (D-079, ADR-012) · v8 (2026-10-02) precedent source *High-Density Housing*, studied in a separate repository, replaces the S7 research (D-082); one plan generator per typology family (D-083); S8 split into S8.1–S8.3 (D-084).
with
> **Revisions:** v1 (2026-09-30) initial draft · v2 (2026-09-30) pipeline-first restructure after D-015 to D-024: Grasshopper objects instead of JSON, the D-018 pipeline vocabulary, ClimateStudio output without simulation, precedent study moved after the pipeline works · v3 (2026-10-01) plan review: conditioning from presets (D-038), explicit windows and a separate window-transformation stage S5 with later stages renumbered (D-039), multiplier without multiplied ground or roof (D-040), enforced façade coverage (D-041); facts established while writing the S0–S4 plans · v4 (2026-10-01) plan review: representative storeys at their true elevations (D-045), exposed storeys of multiplied floor types split off automatically so floor types may differ in footprint (D-046, supersedes D-040), loads aggregated per load type and basis (D-047) · v5 (2026-10-01) Checkpoint 1 review: revision stage S4.1 before S5 (D-061 to D-064) · v6 (2026-10-01) S4.1 review: revision stage S4.2 before S5 (D-068 to D-077) · v7 (2026-10-01) windows of merged zones: one centred window per rebuilt wall with the wall's glazed area, window levels W1–W5 dropped, S5 renamed "Centred windows on rebuilt walls" (D-079, ADR-012) · v8 (2026-10-02) precedent source *High-Density Housing*, studied in a separate repository, replaces the S7 research (D-082); one plan generator per typology family (D-083); S8 split into S8.1–S8.3 (D-084) · v9 (2026-10-02) four-source precedent set `SYN-TYP-001`–`019` (D-093), non-residential programs in scope (D-094), plans per storey (D-095), `013` set aside (D-096), department zoning (D-097), sub-stages S8.4–S8.7 before S6 (D-098).
- Replace
**Goal:** A Rhino 8 Grasshopper plugin that generates detailed residential plans from program presets, simplifies them into lower-LoD buildings that satisfy prescribed conservation invariants, checks those invariants numerically, and outputs Grasshopper objects ready for visualization and for a ClimateStudio definition.
with
**Goal:** A Rhino 8 Grasshopper plugin that generates detailed plans of residential and non-residential building typologies (D-094) from program presets, simplifies them into lower-LoD buildings that satisfy prescribed conservation invariants, checks those invariants numerically, and outputs Grasshopper objects ready for visualization and for a ClimateStudio definition.
- Replace
→ PlanGenerator (LinearPlanGenerator, ...) → IGeneratedPlan Z0: one zone per dwelling unit, explicit windows
with
→ PlanGenerator (LinearPlanGenerator, ...) → IGeneratedPlan Z0: one zone per dwelling unit or department, explicit windows
- Replace
- Z0 has at most one zone per dwelling unit; rooms are never modelled (D-009).
with
- Z0 has at most one zone per dwelling unit and storey; rooms are never modelled (D-009, D-085). A non-residential floor has one zone per department, a contiguous group of rooms of one program (D-097, ADR-014).
- Replace
| S6 | Convert2IDF | S4.2 | `v0.6.0` |
with
| S6 | Convert2IDF | S8.7 (D-098) | `v0.6.0` |
- Replace
| S9 | Grasshopper UX, examples, release | S6, S8.3 | `v1.0.0` |
with
| S8.4 | Non-residential programs, office plate and cafeteria (pinned four-source records, scope alignment, ADR-014; families 017, 016) | S8.3 | `v0.8.3` |
| S8.5 | Branching plans on a shared hub-and-arms layout (families 010, 014, 018, 019) | S8.4 | `v0.8.4` |
| S8.6 | Operating suite and linked court clusters (families 015, 011) | S8.5 | `v0.8.5` |
| S8.7 | Plans that differ by storey (D-095, with an ADR) and stepped bands with terraces (family 012) | S8.6 | `v0.8.6` |
| S9 | Grasshopper UX, examples, release | S6, S8.7 | `v1.0.0` |
- Replace
S0 ── S1 ── S2 ── S3 ── S4 ── S4.1 ── S4.2 ──┬── S5 Windows ── S8.1 ── S8.2 ── S8.3 ──┐
└── S6 Convert2IDF ──────────────────────┴── S9 Release
with
S0 ── S1 ── S2 ── S3 ── S4 ── S4.1 ── S4.2 ── S5 Windows ── S8.1 ── S8.2 ── S8.3 ── S8.4 ── S8.5 ── S8.6 ── S8.7 ── S6 Convert2IDF ── S9 Release
- Replace
The pipeline is complete for the linear plan at the end of S4 (D-015); only then do the centred-window rule (S5), precedents, and new typologies begin. The revision stage S4.1 (D-061 to D-064) changes the Grasshopper inputs after the Checkpoint 1 review (one preset input per space type with default preset components, a *Preview Location* input, icons) and runs before S5. The revision stage S4.2 (D-068 to D-077) follows it, also before S5: floors and ceilings designated on the assembled stack, four floor aggregation methods, *Transform Plan*, and heat transfer between zones as an export option. S6 depends on S4.2 for that option (D-077). The precedent research of S7 was done in a separate repository (D-082); S8 builds one plan generator per typology family in three sub-stages (D-083, D-084). Simulation, batch experiments, and error analysis are outside this repository (D-016).
with
The pipeline is complete for the linear plan at the end of S4 (D-015); only then do the centred-window rule (S5), precedents, and new typologies begin. The revision stage S4.1 (D-061 to D-064) changes the Grasshopper inputs after the Checkpoint 1 review (one preset input per space type with default preset components, a *Preview Location* input, icons) and runs before S5. The revision stage S4.2 (D-068 to D-077) follows it, also before S5: floors and ceilings designated on the assembled stack, four floor aggregation methods, *Transform Plan*, and heat transfer between zones as an export option. S6 depends on S4.2 for that option (D-077) and follows S8.7 (D-098). The precedent research of S7 was done in a separate repository (D-082); S8 builds one plan generator per typology family in three sub-stages (D-083, D-084); the four-source precedent set (D-093) adds nine families of every program type (D-094), built in S8.4–S8.7 before S6 (D-098). Simulation, batch experiments, and error analysis are outside this repository (D-016).
- Replace
EnclosedCourt/ (S8.3: 005)
with
EnclosedCourt/ (S8.3: 005)
OfficePlate/ Cafeteria/ (S8.4: SYN-TYP-017, 016)
HubArms/ RadialLobes/ BranchingMall/ CareHub/ FoyerHalls/ (S8.5: 010, 014, 018, 019)
OperatingSuite/ CourtCluster/ (S8.6: 015, 011)
SteppedBand/ (S8.7: 012)
- Replace
### S8 — Typologies (S8.1–S8.3, D-083, D-084)
with
### S8 — Typologies (S8.1–S8.7, D-083, D-084, D-098)
- Replace
**Goal:** one plan generator per *High-Density Housing* family `HDHM-TYP-001` to `HDHM-TYP-009` (D-083), each a `PlanGenerator` subclass with typed parameter and preset records ([ADR-006](../decisions/ADR-006-plan-generation-mechanism.md)), and a Grasshopper component per generator in panel 2 Generate. Dimensions and counts are design defaults recorded in ADR-006; the gallery of 002 is not modelled. Each sub-stage follows the S5 method (D-080): verified reference, headless Rhino check, plan, per-task execution, local merge and tag; nothing is pushed during the run.
with
**Goal:** one plan generator per *High-Density Housing* family `HDHM-TYP-001` to `HDHM-TYP-009` (D-083, S8.1–S8.3), and from S8.4 one per family of the four-source set `SYN-TYP-010` to `SYN-TYP-019` except `013` (D-093, D-096, D-098), each a `PlanGenerator` subclass with typed parameter and preset records ([ADR-006](../decisions/ADR-006-plan-generation-mechanism.md)), and a Grasshopper component per generator in panel 2 Generate. Dimensions and counts are design defaults recorded in ADR-006; the gallery of 002 is not modelled. Each sub-stage follows the S5 method (D-080): verified reference, headless Rhino check, plan, per-task execution, local merge and tag; nothing is pushed during the run.
- Replace
- **S8.3 Enclosed courtyard (`v0.8.2`).** *Enclosed Court* (005): footprints with holes supported by every plan simplifier (perimeter/core included) and floor aggregator. The court is outside the building: its edges are outdoor walls, and the surface builder requires every footprint edge, court edges included, to be covered by outdoor zone edges; *Perimeter Core* zones the court façades separately (`P-Court-<Bin>`), and its width precondition applies to the wings between the outer and the court façades (D-087). The headless Rhino check of the reference found the *Convert2BEM* zone solids inward-oriented and the floors facing up on every plan released up to `v0.8.1`; S8.3 applies the orientation rule of D-091.
with
- **S8.3 Enclosed courtyard (`v0.8.2`).** *Enclosed Court* (005): footprints with holes supported by every plan simplifier (perimeter/core included) and floor aggregator. The court is outside the building: its edges are outdoor walls, and the surface builder requires every footprint edge, court edges included, to be covered by outdoor zone edges; *Perimeter Core* zones the court façades separately (`P-Court-<Bin>`), and its width precondition applies to the wings between the outer and the court façades (D-087). The headless Rhino check of the reference found the *Convert2BEM* zone solids inward-oriented and the floors facing up on every plan released up to `v0.8.1`; S8.3 applies the orientation rule of D-091.
- **S8.4 Non-residential programs, office plate and cafeteria (`v0.8.3`).** The four-source records pinned (D-093, [syn-typologies.md](../research/precedents/syn-typologies.md)); the scope of D-094 in the research brief, spec, README, AGENTS.md, and this roadmap; [ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md) (the non-residential space types, one typed illustrative preset each, and the departments of every new family, D-097); ADR-006 extended to all nine new families; the default preset components of the new space types; *Office Plate* (`SYN-TYP-017`, a core inside one ring-shaped work-area zone) and *Cafeteria* (`SYN-TYP-016`, a kitchen and servery edge beside the dining field).
- **S8.5 Branching plans (`v0.8.4`).** A shared hub-and-arms layout for *Radial Lobes* (010), *Branching Mall* (014), *Care Hub* (018), and *Foyer Halls* (019).
- **S8.6 Operating suite and court clusters (`v0.8.5`).** *Operating Suite* (015, separate clean and dirty corridors) and *Court Cluster* (011, a footprint with several courts).
- **S8.7 Plans per storey (`v0.8.6`).** A plan generator that returns one plan per storey (D-095, with its ADR: the interface, how the four floor aggregators treat storeys that differ, and how a terrace is designated) and *Stepped Band* (012).
- [x] Step 6: Edit the roadmap links of the S8.1, S8.2, and S8.3 plans
In docs/plans/2026-10-02-s8.1-bar-typologies.md, make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s83-d-083-d-084)
with
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s87-d-083-d-084-d-098)
In docs/plans/2026-10-02-s8.2-non-rectangular-typologies.md, make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s83-d-083-d-084)
with
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s87-d-083-d-084-d-098)
In docs/plans/2026-10-02-s8.3-enclosed-courtyard.md, make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s83-d-083-d-084)
with
(2026-09-30-implementation-roadmap.md#s8--typologies-s81s87-d-083-d-084-d-098)
- [x] Step 7: Check the links and commit
git grep -n "s8--typologies-s81s83-d-083-d-084" -- docs ":!docs/plans/2026-10-02-s8.4-non-residential-programs.md"
git grep -c "s8--typologies-s81s87-d-083-d-084-d-098" -- docs/plans/2026-10-02-s8.1-bar-typologies.md docs/plans/2026-10-02-s8.2-non-rectangular-typologies.md docs/plans/2026-10-02-s8.3-enclosed-courtyard.md
git grep -c "ADR-014-program-types-and-department-zoning.md" -- AGENTS.md docs/LOD_grasshopper_plugin_repository_spec.md docs/plans/2026-09-30-implementation-roadmap.md
git add AGENTS.md README.md docs/LOD_geometric_zoning_equivalence_research_brief.md docs/LOD_grasshopper_plugin_repository_spec.md docs/plans/2026-09-30-implementation-roadmap.md docs/plans/2026-10-02-s8.1-bar-typologies.md docs/plans/2026-10-02-s8.2-non-rectangular-typologies.md docs/plans/2026-10-02-s8.3-enclosed-courtyard.md
git commit -m "docs(research): widen the study to typologies of every program type"
Expected: no output from the first command (no link to the old anchor is left); docs/plans/2026-10-02-s8.1-bar-typologies.md:1, docs/plans/2026-10-02-s8.2-non-rectangular-typologies.md:1, docs/plans/2026-10-02-s8.3-enclosed-courtyard.md:1 from the second; and AGENTS.md:1, docs/LOD_grasshopper_plugin_repository_spec.md:1, docs/plans/2026-09-30-implementation-roadmap.md:1 from the third.
Task 4: ADR-006 specifies the families of the four-source set¶
Files:
- Modify: docs/decisions/ADR-006-plan-generation-mechanism.md (S8.1, last changed in S8.3)
Interfaces:
- Consumes: D-093 to D-098; the precedent page (Task 1); ADR-014 (Task 2).
- Produces: ADR-006's status line extended to the four-source set; short edits to its Context and Zones bullets; the section ### Families of the four-source set (S8.4–S8.7) with its additions (department zones, controller design defaults, per-family floor heights, the upper bound of the hub-and-arms layout, recorded as D-100 at the close-out), one subsection per new family (017 and 016 for S8.4; the shared hub-and-arms block and 010, 014, 018, 019 for S8.5; 015 and 011 for S8.6; 012 for S8.7) with a table of parameters, defaults, and reasons, the presets, zone IDs, realisation by rule ID, validation, and what is not modelled; and three new Consequences bullets. The records, defaults, and rules of the nine existing families do not change.
- [x] Step 1: Edit
docs/decisions/ADR-006-plan-generation-mechanism.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
Status: Accepted (by the controller under D-084; provisional pending the owner's reading)
with
Status: Accepted (by the controller under D-084; provisional pending the owner's reading); extended to the families of the four-source set in S8.4 (by the controller under D-098)
- Replace
Decisions: D-009, D-026, D-061, D-065, D-082, D-083, D-084; applies [ADR-002](ADR-002-geometry.md), [ADR-008](ADR-008-perimeter-core-corners.md), and [ADR-011](ADR-011-minimum-wall-and-window-geometry.md)
with
Decisions: D-009, D-026, D-061, D-065, D-082, D-083, D-084, D-085, D-093 to D-098; applies [ADR-014](ADR-014-program-types-and-department-zoning.md), [ADR-002](ADR-002-geometry.md), [ADR-008](ADR-008-perimeter-core-corners.md), and [ADR-011](ADR-011-minimum-wall-and-window-geometry.md)
- Replace
Since S2 one plan generator exists, `LinearPlanGenerator`, built on the base class `PlanGenerator<TParameters, TPresets>`: the subclass validates its parameters and lays out zones as polygons; the base checks the presets (D-061), builds the surfaces, places the windows (D-026), and records provenance. The precedent study (D-082) gives nine typology families, `HDHM-TYP-001` to `HDHM-TYP-009`, as topological rules without dimensions or counts ([precedent page](../research/precedents/hdhm-typologies.md)). D-083 asks for one generator per family, with the dimensions and counts chosen by the controller as design defaults and recorded here, and with each generator's documentation stating how it realises its family's rules. D-084 implements the bar families (009 checked, 001, 008, 002, 006) in S8.1, the non-rectangular families without holes (007, 003, 004) in S8.2, and the enclosed courtyard (005) in S8.3. This ADR fixes the mechanism and the parameter and preset records of all nine families, so that S8.2 and S8.3 only implement them.
with
Since S2 one plan generator exists, `LinearPlanGenerator`, built on the base class `PlanGenerator<TParameters, TPresets>`: the subclass validates its parameters and lays out zones as polygons; the base checks the presets (D-061), builds the surfaces, places the windows (D-026), and records provenance. The precedent study (D-082) gives nine typology families, `HDHM-TYP-001` to `HDHM-TYP-009`, as topological rules without dimensions or counts ([precedent page](../research/precedents/hdhm-typologies.md)). D-083 asks for one generator per family, with the dimensions and counts chosen by the controller as design defaults and recorded here, and with each generator's documentation stating how it realises its family's rules. D-084 implements the bar families (009 checked, 001, 008, 002, 006) in S8.1, the non-rectangular families without holes (007, 003, 004) in S8.2, and the enclosed courtyard (005) in S8.3. This ADR fixes the mechanism and the parameter and preset records of all nine families, so that S8.2 and S8.3 only implement them. Since S8.4 the precedent set is the four-source family set `SYN-TYP-001` to `SYN-TYP-019` (D-093), which carries the nine families as `SYN-TYP-001` to `SYN-TYP-009` and adds families of every program type (D-094); the section [Families of the four-source set](#families-of-the-four-source-set-s84s87) specifies them in the same way. Family IDs below are the `HDHM-TYP-*` IDs of S8.1–S8.3, which are `SYN-TYP-*` with the same number.
- Replace
- **Zones.** One zone per dwelling unit and storey; dwellings are never subdivided into rooms (D-009). Non-dwelling spaces are their own zones. Space types are only `DwellingUnit`, `Stair`, and `Corridor`, so the existing default preset components serve every family; a point-access core is a `Stair` zone. Zone IDs are short, stable, and numbered from 1 west to east (along a wing's axis for wings), like the linear plan's `ST`, `CO`, `US1…`, `UN1…`; display names spell them out (`Unit 3`, `Stair 2`).
with
- **Zones.** One zone per dwelling unit and storey; dwellings are never subdivided into rooms (D-009). Non-dwelling spaces are their own zones. The housing families of S8.1–S8.3 use only the space types `DwellingUnit`, `Stair`, and `Corridor`, so the residential default preset components serve them (the non-residential families zone per department with the space types of ADR-014, below); a point-access core is a `Stair` zone. Zone IDs are short, stable, and numbered from 1 west to east (along a wing's axis for wings), like the linear plan's `ST`, `CO`, `US1…`, `UN1…`; display names spell them out (`Unit 3`, `Stair 2`).
- Replace
- *No Simplification*, *Semantic Merge*, *Single Zone per Floor*, the four floor aggregators, validation, and the window rule needed no change: united zones and storey outlines keep the hole, the court has neither a ground slab nor a roof because it is outside every footprint, rebuilt court walls get one centred window each (D-079), and glazing per orientation counts the court façades by the direction they face ([ADR-013](ADR-013-vertical-aggregation-methods.md)). Every existing snapshot is byte-identical.
## Consequences
with
- *No Simplification*, *Semantic Merge*, *Single Zone per Floor*, the four floor aggregators, validation, and the window rule needed no change: united zones and storey outlines keep the hole, the court has neither a ground slab nor a roof because it is outside every footprint, rebuilt court walls get one centred window each (D-079), and glazing per orientation counts the court façades by the direction they face ([ADR-013](ADR-013-vertical-aggregation-methods.md)). Every existing snapshot is byte-identical.
### Families of the four-source set (S8.4–S8.7)
The four-source precedent set (D-093, [syn-typologies.md](../research/precedents/syn-typologies.md)) carries `SYN-TYP-001` to `SYN-TYP-009` as the nine families above (lineage `HDHM-TYP-001` to `HDHM-TYP-009`, same organisation and rule numbering), so their generators, records, and defaults are unchanged. It adds nine families that get generators (D-094, D-096): the housing families `010`, `011`, and `012` and the non-residential families `014` to `019`; `SYN-TYP-013` is set aside (D-096). This section specifies all nine now, as S8.1 specified the families of S8.2 and S8.3, so that S8.5–S8.7 only implement them; a sub-stage that refines a layout records the change here. D-098 assigns them: `017` and `016` in S8.4, `010`, `014`, `018`, and `019` in S8.5 on a shared hub-and-arms layout, `015` and `011` in S8.6, and `012` in S8.7 with plans per storey (D-095).
The mechanism above applies unchanged, with three additions:
- **Zones.** Housing families keep one zone per dwelling and storey (D-009, D-085); non-residential families place one zone per department (D-097), with the space types and the rooms each zone stands for set in [ADR-014](ADR-014-program-types-and-department-zoning.md). A department zone may surround another zone (a ring); it is never split into rooms or tenancies.
- **Defaults.** The source supports no metric or count range for any family (D-093), so every default below is a controller design default (D-098): a plausible dimension for the building type, not a value from the source, with its reason. Floor heights are chosen per family (3 m for housing, more where the program needs deeper services or tall rooms); orientation is 0° for every family.
- **Upper bounds.** The layouts are axis-aligned in the plan frame. Where a family's branches are arms on the four sides of a hub, at most four fit; a larger count needs non-orthogonal arms and is not supported (`InvalidParameter`). This is a limit of the layout, not of the family.
#### `SYN-TYP-017` Central service core within an office plate: `OfficePlateGenerator` (S8.4)
| Parameter | Default | Reason |
| --- | --- | --- |
| `PlateLength` | 42 m | a rectangular office plate: 12 m of office depth on both sides of an 18 m core |
| `PlateDepth` | 30 m | 12 m of office depth on both sides of a 6 m core, so every work area is daylit from one façade |
| `CoreLength` | 18 m | lifts, stairs, toilets, and risers in one core along the plate's long axis |
| `CoreDepth` | 6 m | two rows of service rooms either side of a lift lobby |
| `FloorHeight` | 3.8 m | a raised floor and a suspended ceiling with services |
- Presets `OfficePlatePresets(Office, Core)`. The plate is the rectangle `[0, PlateLength] × [0, PlateDepth]` (1260 m² by default); the core is centred on it, `[(L − l)/2, (L + l)/2] × [(D − d)/2, (D + d)/2]` for a plate `L × D` and a core `l × d` (108 m²). Zones `SC` (*Service Core*, `Core`), then `OF` (*Office*, `Office`): the office work area is one zone, the plate minus the core, a polygon with the core as its hole (1152 m²); the footprint has no hole.
- Realisation: `R001` office work areas gather around the central core: the work-area zone surrounds the core on all four sides; the several work areas of the precedents form one contiguous department and are one zone (D-097). `R002` lifts and shared services lie in the central core, a `Core` zone without an outdoor wall. `R003`/`R004`: the core must be central, with work area on every side.
- Validation: every length positive; the core leaves an office depth of at least `OfficePlateGenerator.MinimumOfficeDepth` = 3 m on every side, `(PlateLength − CoreLength)/2` at the west and east façades and `(PlateDepth − CoreDepth)/2` at the south and north façades: a core closer to a façade is the edge-core plan that the source records outside the family, and 3 m is the shallowest band that holds a row of desks with its aisle. For example `CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.`
- Not modelled: individual work areas, meeting rooms, and the rooms of the core (D-097); a core off the centre; non-rectangular plates. *Perimeter Core* still needs every part of the plate wider than twice its depth (D-087); the default plate passes with 4.57 m.
#### `SYN-TYP-016` Cafeteria service edge beside a dining field: `CafeteriaGenerator` (S8.4)
| Parameter | Default | Reason |
| --- | --- | --- |
| `Length` | 30 m | the length of the service edge and of the dining field it borders |
| `DiningDepth` | 15 m | a dining field of 450 m², about 300 seats at 1.5 m² each |
| `KitchenDepth` | 8 m | preparation, cooking, wash-up, and a servery counter along the kitchen's front |
| `FloorHeight` | 4 m | a tall dining hall and kitchen extract ducts |
- Presets `CafeteriaPresets(Kitchen, Dining)`. The plan is the rectangle `[0, Length] × [0, DiningDepth + KitchenDepth]` (690 m² by default): the dining field `[0, Length] × [0, DiningDepth]` on the plan's south side, which gets the daylight, and the service edge `[0, Length] × [DiningDepth, DiningDepth + KitchenDepth]` on its north side. Zones `KS` (*Kitchen and Servery*, `Kitchen`), then `DI` (*Dining*, `Dining`).
- Realisation: `R001` food preparation and servery lie along an edge of the dining field: the kitchen zone shares the dining field's whole north edge. `R002` customers move past the service edge into the dining space: the servery counter is the kitchen's front on that shared edge, so the route along it lies in the dining field and is not a zone of its own. `R003`/`R004`: the dining field must be the larger part (the precedents' dining field is larger than the service edge it adjoins).
- Validation: every length positive; `KitchenDepth` < `DiningDepth`, for example `KitchenDepth 15 m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.`
- Not modelled: the counter, the queue, the kitchen's rooms (stores, wash-up), seating, and entrances (D-097); a service edge on more than one side.
#### Shared building block: the hub and its arms (S8.5)
`HubArmsLayout` (in `Lod.Generators/HubArms/`, internal) lays out a square hub of side `H` and up to four rectangular arms on its sides, in the order east, north, west, south (counter-clockwise from plan east), each arm centred on its side, of length `Lᵢ` away from the hub and width `A` along the side. An arm must not be wider than the hub (`A ≤ H`; a wider arm would overlap its neighbour beyond the hub's corner), which every family validates. The footprint is the union of hub and arms (a T for three arms, a cross for four), placed with the south-west corner of its bounding box at the origin. A hub side without an arm, and the hub's corners when `A < H`, are outdoor walls. The families subdivide the hub and the arms into their zones; zones are listed hub first, then arm by arm in the order above.
#### `SYN-TYP-010` Radial lobes around a point tower core: `RadialLobesGenerator` (S8.5)
| Parameter | Default | Reason |
| --- | --- | --- |
| `LobeCount` | 4 | a cruciform tower; minimum 3 (two lobes would be a bar through a core), maximum 4 (orthogonal arms) |
| `LobeLength` | 10 m | a dwelling depth from the core to the lobe's end |
| `LobeWidth` | 12 m | two 6 m dwellings side by side in each lobe |
| `CoreWidth` | 12 m | stairs, lifts, and the landing that every dwelling opens onto; at least the lobe width |
| `FloorHeight` | 3 m | housing |
- Presets `RadialLobesPresets(DwellingUnit, Stair)`. The hub is the core, the arms are the lobes (`A` = `LobeWidth`, `H` = `CoreWidth`). Each lobe holds two dwellings side by side along its axis, each spanning the lobe's length and half its width, so each adjoins the core over half the lobe's root. Zones `ST` (*Core*), then `L{i}-U1`, `L{i}-U2` (*Lobe i Unit 1*, …; lobe `i` from 1 in the arm order, `U1` the half clockwise of the lobe's axis). Default plan 624 m²: a 144 m² core and eight 60 m² dwellings.
- Realisation: `R001` dwelling lobes branch around the compact core; `R002` every dwelling adjoins the core (point access); `R003` movement spreads from the core into the lobes, and there is no other circulation zone. Validation: `LobeCount` 3–4; lengths positive; `LobeWidth` ≤ `CoreWidth`. With three lobes the core's free side is an outdoor wall with the stair preset's windows.
- Not modelled: lobes at other than right angles, lobes of different sizes, more than two dwellings per lobe.
#### `SYN-TYP-014` Branching public mall between retail wings: `BranchingMallGenerator` (S8.5)
| Parameter | Default | Reason |
| --- | --- | --- |
| `BranchCount` | 3 | a T of mall branches; minimum 3 (two branches are a bend, not a branching), maximum 4 |
| `BranchLength` | 40 m | the shop wings along each branch |
| `MallWidth` | 8 m | a covered public mall with room for kiosks |
| `ShopDepth` | 12 m | shop units on each side of the mall |
| `AnchorDepth` | 30 m | a large store at the end of each branch, across its full width |
| `FloorHeight` | 5 m | tall shop fronts and a mall roof |
- Presets `BranchingMallPresets(Mall, Retail)`. Arm width `A` = `2·ShopDepth + MallWidth` (32 m) and hub side `H` = `A`; each arm is `BranchLength + AnchorDepth` long. The hub is the mall's central court; each arm holds, from the hub outwards, a shop wing of depth `ShopDepth` on either side of a mall strip of width `MallWidth`, then the anchor across the arm's width. Zones `MALL` (*Mall*: the hub and every mall strip, one polygon), then per branch `B{i}-S1`, `B{i}-S2` (*Branch i Shops 1*, …; the shop wings clockwise and counter-clockwise of the mall strip) and `B{i}-AN` (*Branch i Anchor*). Space types `Mall` for the mall and `Retail` for shop wings and anchors (ADR-014).
- Realisation: `R001` retail wings and anchors lie around the branching mall: every shop wing and anchor adjoins `MALL`; `R002` the mall is one connected, branching route reaching every branch. Validation: `BranchCount` 3–4; lengths positive.
- Not modelled: individual shops and tenancies (D-097), branches at other than right angles, rooflights, servicing yards.
#### `SYN-TYP-018` Communal hub serving elder-care bedroom groups: `CareHubGenerator` (S8.5)
| Parameter | Default | Reason |
| --- | --- | --- |
| `WingCount` | 3 | three bedroom groups around the hub; minimum 2 (groups, plural), maximum 4 |
| `WingLength` | 30 m | about eight bedrooms of 3.6 m along each side of a wing |
| `BedroomDepth` | 6 m | a care bedroom with its en-suite |
| `CorridorWidth` | 2.4 m | passing wheelchairs and beds |
| `HubWidth` | 16 m | lounge, dining, care station, and kitchenette; at least the wing width |
| `FloorHeight` | 3 m | residential scale |
- Presets `CareHubPresets(CareBedroom, CareCommunal, Corridor)`. Arm width `A` = `2·BedroomDepth + CorridorWidth` (14.4 m), `H` = `HubWidth`. Each wing is a corridor along its axis, from the hub to the wing's end, with a bedroom strip on each side. Zones `HUB` (*Communal Hub*), then per wing `W{i}-CO` (*Wing i Corridor*), `W{i}-B1`, `W{i}-B2` (*Wing i Bedrooms 1*, …).
- Realisation: `R001` bedroom groups are arranged around the communal and service hub; `R002` circulation branches from the hub: every wing corridor adjoins it. Validation: `WingCount` 2–4; lengths positive; `2·BedroomDepth + CorridorWidth` ≤ `HubWidth`.
- Not modelled: individual bedrooms (D-097), the care station as a zone of its own, gardens.
#### `SYN-TYP-019` Public foyer branching to distinct activity spaces: `FoyerHallsGenerator` (S8.5)
| Parameter | Default | Reason |
| --- | --- | --- |
| `HallCount` | 2 | two distinct activity spaces; minimum 2, maximum 3 (one arm holds the support rooms) |
| `HallLength` | 30 m | a four-court sports hall or a large community hall |
| `HallWidth` | 20 m | as above |
| `SupportLength` | 10 m | changing rooms, stores, and toilets in one wing |
| `FoyerWidth` | 20 m | a foyer as wide as the halls it serves; at least the hall width |
| `FloorHeight` | 6 m | a hall for sport; the plan has one storey height |
- Presets `FoyerHallsPresets(Lobby, ActivityHall, Service)`. `A` = `HallWidth`, `H` = `FoyerWidth`. Arms in the order east, north, west, south: hall 1 east, the support wing north, hall 2 west, and hall 3 south when `HallCount` is 3; with two halls the foyer's south side is its entrance façade. Zones `FO` (*Foyer*), then `H1…Hn` (*Hall i*), then `SV` (*Support*).
- Realisation: `R001` distinct activity spaces and the support rooms lie around the common foyer; `R002` access branches from the foyer: every hall and the support wing adjoins it. Validation: `HallCount` 2–3; lengths positive; `HallWidth` ≤ `FoyerWidth`. With three halls and `FoyerWidth` = `HallWidth` the foyer has no outdoor wall, which is allowed.
- Not modelled: individual changing rooms and stores (D-097), spectator galleries, halls of different sizes, and different storey heights for halls and foyer.
#### `SYN-TYP-015` Operating suite with separated clean and dirty routes: `OperatingSuiteGenerator` (S8.6)
| Parameter | Default | Reason |
| --- | --- | --- |
| `BankCount` | 2 | repeated theatre banks; minimum 2 |
| `BankLength` | 36 m | four theatres of about 9 m with their anaesthetic rooms in one bank |
| `TheatreDepth` | 7 m | a theatre of about 9 m × 7 m |
| `CleanCorridorWidth` | 3 m | a clean route wide enough for beds |
| `DirtyCorridorWidth` | 2.4 m | a disposal route for trolleys |
| `SupportLength` | 12 m | scrub, stores, recovery, and sluice across the suite's west end |
| `FloorHeight` | 4.2 m | a ceiling plenum for theatre air handling |
- Presets `OperatingSuitePresets(OperatingTheatre, CleanCorridor, DirtyCorridor, ClinicalSupport)`. The support rooms `[0, SupportLength]` span the suite's full depth at its west end; east of them, bands along plan x of length `BankLength` are stacked from south to north: a dirty corridor, then for each bank the bank and the next corridor, the corridors alternating clean and dirty (`DC1`, `TB1`, `CC1`, `TB2`, `DC2`, `TB3`, `CC2`, …), so every bank lies between a clean and a dirty corridor and consecutive banks share a corridor. Zones `SUP` (*Support*), then the bands south to north: `DC{k}` (*Dirty Corridor k*), `TB{k}` (*Theatre Bank k*), `CC{k}` (*Clean Corridor k*).
- Realisation: `R001` repeated theatre banks beside the support rooms: every bank and corridor adjoins `SUP`; `R002` distinct clean and dirty routes: every bank adjoins one clean and one dirty corridor, and no clean corridor adjoins a dirty one. Validation: `BankCount` ≥ 2; lengths positive.
- Not modelled: individual theatres and their anaesthetic and preparation rooms (D-097), pressure cascades (only the presets' ventilation), lifts.
#### `SYN-TYP-011` Linked housing clusters around multiple courts: `CourtClusterGenerator` (S8.6)
| Parameter | Default | Reason |
| --- | --- | --- |
| `CourtCount` | 2 | several shared courts; minimum 2 (one court is `005`) |
| `CourtWidth` | 24 m | twice the wing depth, as `005` |
| `CourtDepth` | 24 m | a square court, as `005` |
| `Depth` | 12 m | wing depth, a through dwelling |
| `UnitWidth` | 7 m | as `001` |
| `StairWidth` | 3 m | as `001` |
| `FloorHeight` | 3 m | housing |
- Presets `CourtClusterPresets(DwellingUnit, Stair)`. A row of `CourtCount` courts along plan x: south and north wings across the full width, then a west wing, `CourtCount − 1` middle wings between neighbouring courts, and an east wing, every wing a bar of stair bays fitted to its length as for `005`. The footprint is the outer rectangle with one hole per court. Zones `S-…`, `N-…` (west to east), then `W-…`, `M{i}-…` (middle wing `i`, west to east), `E-…`, each wing stairs first, then dwellings, as in `005`.
- Realisation: `R001` dwelling wings linked into one cluster around several courts; `R002` the courts lie between dwelling groups (each middle wing borders two courts); `R003` access is distributed: every wing has its own stairs. Validation: `CourtCount` ≥ 2; lengths positive; the stair bays fit every wing (`StairBayLayout.FitErrors`).
- Not modelled: courts that are not in one row or not rectangular, the outdoor routes through the courts, a route hierarchy.
#### `SYN-TYP-012` Stepped dwelling bands with exposed terraces: `SteppedBandGenerator` (S8.7)
| Parameter | Default | Reason |
| --- | --- | --- |
| `StoreyCount` | 3 | a band stepping back twice; minimum 2 |
| `Length` | 36 m | a band of five or six dwellings per storey |
| `BaseDepth` | 14 m | dwelling depth on the ground storey |
| `StepDepth` | 3 m | a terrace deep enough to sit on |
| `CorridorWidth` | 2 m | the shared circulation edge along the band's rear |
| `UnitWidth` | 6 m | target dwelling width; adjusted to fill the band |
| `StairWidth` | 4 m | a stair at the west end |
| `FloorHeight` | 3 m | housing |
- Presets `SteppedBandPresets(DwellingUnit, Corridor, Stair)`. The generator returns one plan per storey (D-095; the interface, how the floor aggregators treat storeys that differ, and the designation of terraces are fixed in the ADR of S8.7). On storey `k` (from 0) the dwellings span `y ∈ [k·StepDepth, BaseDepth]` and `x ∈ [StairWidth, Length]`, divided evenly into `max(1, round((Length − StairWidth) / UnitWidth))` dwellings; the corridor `CO` spans `[0, Length] × [BaseDepth, BaseDepth + CorridorWidth]` on every storey and the stair `ST` `[0, StairWidth] × [k·StepDepth, BaseDepth]`. The part of storey `k − 1`'s roof that storey `k` does not cover, `[0, Length] × [(k − 1)·StepDepth, k·StepDepth]`, is a terrace: an outdoor roof (D-068). Zones per storey `ST`, `CO`, `U1…Un` (west to east).
- Realisation: `R001` the band steps back storey by storey, so that lower roofs become terraces; `R002` every dwelling adjoins the shared corridor along the rear. Validation: `StoreyCount` ≥ 2; lengths positive (`StepDepth` > 0: the stepping is essential); `BaseDepth − (StoreyCount − 1)·StepDepth` > 0, so the top storey keeps dwellings.
- Not modelled: terrace parapets, planting, and shading; an internal gallery instead of a rear corridor; steps on both sides of the band.
## Consequences
- Replace
- The court of `HDHM-TYP-005` is outside the building: its façades are outdoor walls with windows, and it has no ground slab, roof, or zone.
with
- The court of `HDHM-TYP-005` is outside the building: its façades are outdoor walls with windows, and it has no ground slab, roof, or zone.
- The four-source families (S8.4–S8.7) are specified before they are built; their defaults are controller design defaults (D-098), and every non-residential zone is a department (D-097, ADR-014), so a non-residential detailed model has fewer and larger zones than a room-level model.
- The office plate's work area is the first zone with a hole whose footprint has none (a ring around the core); the surface builder, every simplifier, and every floor aggregator handle it (S8.4).
- Arms on the sides of a hub are limited to four by the axis-aligned layout (S8.5); counts beyond the limits in the tables are `InvalidParameter`.
- [x] Step 2: Check and commit
git grep -c -e "^#### \`SYN-TYP-0" -- docs/decisions/ADR-006-plan-generation-mechanism.md
git grep -c "### Families of the four-source set (S8.4–S8.7)" -- docs/decisions/ADR-006-plan-generation-mechanism.md
git add docs/decisions/ADR-006-plan-generation-mechanism.md
git commit -m "docs(decisions): specify the families of the four-source set in adr-006"
Expected: docs/decisions/ADR-006-plan-generation-mechanism.md:9 (the nine new family subsections) from the first command and docs/decisions/ADR-006-plan-generation-mechanism.md:1 from the second.
- [x] Step 3: Merge Slice A (D-004)
git fetch origin
git switch main
git pull --ff-only
git switch docs/four-source-scope
git rebase main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
git switch main
git merge --ff-only docs/four-source-scope
git branch -d docs/four-source-scope
Expected: VERIFY PASSED, with 158 core, 181 generators, and 603 integration tests passed on net8.0. Do not push (D-098: the stage is pushed in Task 13).
Slice B — feature/non-residential-programs¶
Task 5: Non-residential space types and example presets¶
Files:
- Modify: src/Lod.Core/Programs/ZoneProgram.cs (S1 file)
- Create: src/Lod.Core/Programs/ExampleNonResidentialPresets.cs
- Test: create tests/Lod.Core.Tests/Programs/ExampleNonResidentialPresetsTests.cs
Interfaces:
- Consumes (S1, S4.1): SpaceType; PresetValues(string Name, SpaceType SpaceType, double WindowToWallRatio, bool Conditioned, double HeatingSetpoint, double CoolingSetpoint, IReadOnlyList<PresetLoadValue> Loads) with ToPreset() : Result<ProgramPreset>; PresetLoadValue(LoadType Type, LoadBasis Basis, double Value, Schedule Schedule); Schedule.Create(string name, ScheduleKind kind, IEnumerable<double> values, AggregationRecord? aggregation = null), Schedule.Constant(string name, ScheduleKind kind, double value), Schedule.FromDailyProfiles(string name, ScheduleKind kind, IReadOnlyList<double> weekday, IReadOnlyList<double> weekend, DayOfWeek firstDayOfYear), Schedule.HoursPerYear; LoadType (including GasEquipment and Ventilation), LoadBasis.PerFloorArea and AirChangesPerHour; the pattern of ExampleResidentialPresets; ADR-014 (Task 2).
- Produces: twelve SpaceType values appended after Mixed, each with an XML summary citing ADR-014, and clarified summaries of Core, Lobby, and Service; public static class ExampleNonResidentialPresets with OfficeValues, CoreValues, RetailValues, MallValues, KitchenValues, DiningValues, OperatingTheatreValues, CleanCorridorValues, DirtyCorridorValues, ClinicalSupportValues, CareBedroomValues, CareCommunalValues, LobbyValues, ActivityHallValues, ServiceValues (PresetValues, named Example Office, …), the fifteen matching ProgramPreset properties (Office, Core, …), SpaceTypes : IReadOnlyList<SpaceType> (the fifteen types in ADR-014's order), and ValuesOf(SpaceType) and PresetOf(SpaceType), which throw ArgumentOutOfRangeException for any other type. The schedules are 8760-hour profiles of a year starting on a Monday: weekday and weekend profiles through Schedule.FromDailyProfiles, the retail profiles with their own Saturday and Sunday through a private weekly helper on Schedule.Create, the care profiles the same every day, and the shared Example Always On. No public API other than these is added.
- [x] Step 1: Create the branch
git switch -c feature/non-residential-programs main
- [x] Step 2: Write the failing tests — create
tests/Lod.Core.Tests/Programs/ExampleNonResidentialPresetsTests.cs(full file)
35 tests: the enum order with the twelve new values after Mixed; for each of the fifteen presets its name, space type, WWR, conditioning, and setpoints (ValuesHoldTheIllustrativeDefaults) and the preset built from its values (PresetsAreBuiltFromTheirValues); every preset's loads with type, basis, value, schedule name, and annual full-load hours; the retail schedules trading longest on Saturday; the care schedules the same every day; ventilation only for the kitchen and the operating theatre.
using System;
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Loads;
using Lod.Core.Programs;
using Xunit;
namespace Lod.Core.Tests.Programs;
public sealed class ExampleNonResidentialPresetsTests
{
private static readonly Dictionary<SpaceType, (LoadType Type, LoadBasis Basis, double Value, string Schedule, double AnnualFraction)[]> ExpectedLoads = new()
{
[SpaceType.Office] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, "Example Office Occupancy", 2231.55),
(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, "Example Office Lighting", 2695.65),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 10.0, "Example Office Equipment", 3526.8),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.Core] = new[]
{
(LoadType.Lighting, LoadBasis.PerFloorArea, 5.0, "Example Office Lighting", 2695.65),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 3.0, "Example Office Equipment", 3526.8),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.Retail] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.2, "Example Retail Occupancy", 2414.4),
(LoadType.Lighting, LoadBasis.PerFloorArea, 15.0, "Example Retail Lighting", 4547.5),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 5.0, "Example Retail Equipment", 5100.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.5, "Example Always On", 8760.0),
},
[SpaceType.Mall] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, "Example Retail Occupancy", 2414.4),
(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, "Example Retail Lighting", 4547.5),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.8, "Example Always On", 8760.0),
},
[SpaceType.Kitchen] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.05, "Example Kitchen Staff", 2244.6),
(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, "Example Kitchen Hours", 2610.0),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 30.0, "Example Kitchen Electric", 2952.6),
(LoadType.GasEquipment, LoadBasis.PerFloorArea, 40.0, "Example Kitchen Gas", 1122.3),
(LoadType.Ventilation, LoadBasis.AirChangesPerHour, 15.0, "Example Kitchen Hours", 2610.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.Dining] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.6, "Example Dining Occupancy", 809.1),
(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, "Example Dining Hours", 2349.0),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 2.0, "Example Dining Hours", 2349.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.OperatingTheatre] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, "Example Theatre Sessions", 2871.0),
(LoadType.Lighting, LoadBasis.PerFloorArea, 25.0, "Example Theatre Services", 4048.8),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 30.0, "Example Theatre Services", 4048.8),
(LoadType.Ventilation, LoadBasis.AirChangesPerHour, 20.0, "Example Theatre Ventilation", 5946.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.1, "Example Always On", 8760.0),
},
[SpaceType.CleanCorridor] = new[]
{
(LoadType.Lighting, LoadBasis.PerFloorArea, 8.0, "Example Always On", 8760.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.DirtyCorridor] = new[]
{
(LoadType.Lighting, LoadBasis.PerFloorArea, 8.0, "Example Always On", 8760.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.ClinicalSupport] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.05, "Example Theatre Sessions", 2871.0),
(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, "Example Theatre Services", 4048.8),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 10.0, "Example Theatre Services", 4048.8),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.CareBedroom] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.04, "Example Care Bedroom Occupancy", 5803.5),
(LoadType.Lighting, LoadBasis.PerFloorArea, 6.0, "Example Care Bedroom Lighting", 2226.5),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 3.0, "Example Care Bedroom Equipment", 4380.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.CareCommunal] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.15, "Example Care Communal Occupancy", 3777.75),
(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, "Example Care Communal Hours", 6132.0),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 5.0, "Example Care Communal Hours", 6132.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.Lobby] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, "Example Community Occupancy", 2444.5),
(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, "Example Community Hours", 5267.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.6, "Example Always On", 8760.0),
},
[SpaceType.ActivityHall] = new[]
{
(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, "Example Community Occupancy", 2444.5),
(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, "Example Community Hours", 5267.0),
(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 2.0, "Example Community Hours", 5267.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
[SpaceType.Service] = new[]
{
(LoadType.Lighting, LoadBasis.PerFloorArea, 6.0, "Example Community Hours", 5267.0),
(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, "Example Always On", 8760.0),
},
};
public static IEnumerable<object[]> Examples() => new[]
{
new object[] { "Example Office", SpaceType.Office, 0.4, 21.0, 24.0 },
new object[] { "Example Core", SpaceType.Core, 0.0, 21.0, 24.0 },
new object[] { "Example Retail", SpaceType.Retail, 0.3, 20.0, 24.0 },
new object[] { "Example Mall", SpaceType.Mall, 0.2, 20.0, 24.0 },
new object[] { "Example Kitchen", SpaceType.Kitchen, 0.1, 18.0, 26.0 },
new object[] { "Example Dining", SpaceType.Dining, 0.4, 21.0, 24.0 },
new object[] { "Example Operating Theatre", SpaceType.OperatingTheatre, 0.0, 20.0, 22.0 },
new object[] { "Example Clean Corridor", SpaceType.CleanCorridor, 0.1, 20.0, 24.0 },
new object[] { "Example Dirty Corridor", SpaceType.DirtyCorridor, 0.1, 20.0, 24.0 },
new object[] { "Example Clinical Support", SpaceType.ClinicalSupport, 0.2, 21.0, 24.0 },
new object[] { "Example Care Bedroom", SpaceType.CareBedroom, 0.3, 22.0, 25.0 },
new object[] { "Example Care Communal", SpaceType.CareCommunal, 0.4, 22.0, 25.0 },
new object[] { "Example Lobby", SpaceType.Lobby, 0.5, 20.0, 24.0 },
new object[] { "Example Activity Hall", SpaceType.ActivityHall, 0.2, 18.0, 25.0 },
new object[] { "Example Service", SpaceType.Service, 0.1, 18.0, 26.0 },
};
[Fact]
public void NewSpaceTypesAreAppendedAfterTheExistingOnes()
{
Assert.Equal(
new[]
{
"DwellingUnit", "Corridor", "Stair", "Core", "Lobby", "Service", "Mechanical", "Other", "Mixed",
"Office", "Retail", "Mall", "Kitchen", "Dining", "OperatingTheatre", "CleanCorridor", "DirtyCorridor", "ClinicalSupport",
"CareBedroom", "CareCommunal", "ActivityHall",
},
Enum.GetNames(typeof(SpaceType)));
}
[Theory]
[MemberData(nameof(Examples))]
public void ValuesHoldTheIllustrativeDefaults(string name, SpaceType spaceType, double wwr, double heating, double cooling)
{
PresetValues values = ExampleNonResidentialPresets.ValuesOf(spaceType);
Assert.Equal(name, values.Name);
Assert.Equal(spaceType, values.SpaceType);
Assert.Equal(wwr, values.WindowToWallRatio);
Assert.True(values.Conditioned);
Assert.Equal(heating, values.HeatingSetpoint);
Assert.Equal(cooling, values.CoolingSetpoint);
}
[Theory]
[MemberData(nameof(Examples))]
public void PresetsAreBuiltFromTheirValues(string name, SpaceType spaceType, double wwr, double heating, double cooling)
{
PresetValues values = ExampleNonResidentialPresets.ValuesOf(spaceType);
ProgramPreset preset = ExampleNonResidentialPresets.PresetOf(spaceType);
Assert.True(values.ToPreset().IsSuccess);
Assert.Equal(name, preset.Name);
Assert.Equal(spaceType, preset.SpaceType);
Assert.Equal(wwr, preset.WindowToWallRatio);
Assert.True(preset.Program.IsConditioned);
Assert.Equal(
values.Loads.Select(l => (l.Type, l.Basis, l.Value, l.Schedule)).OrderBy(l => l.Type).ThenBy(l => l.Basis),
preset.Program.Loads.Select(l => (l.Type, l.Basis, l.Value, l.Schedule)));
Assert.Equal($"{name} Heating Setpoint", preset.Program.Thermostat!.HeatingSetpoint.Name);
Assert.Equal($"{name} Cooling Setpoint", preset.Program.Thermostat.CoolingSetpoint.Name);
Assert.All(preset.Program.Thermostat.HeatingSetpoint.Values, v => Assert.Equal(heating, v));
Assert.All(preset.Program.Thermostat.CoolingSetpoint.Values, v => Assert.Equal(cooling, v));
}
[Fact]
public void EveryPresetHasItsLoads()
{
Assert.Equal(ExpectedLoads.Keys.OrderBy(t => t), ExampleNonResidentialPresets.SpaceTypes.OrderBy(t => t));
foreach (KeyValuePair<SpaceType, (LoadType Type, LoadBasis Basis, double Value, string Schedule, double AnnualFraction)[]> expected in ExpectedLoads)
{
Assert.Equal(expected.Value, Describe(ExampleNonResidentialPresets.ValuesOf(expected.Key)));
}
}
[Fact]
public void RetailSchedulesTradeLongestOnSaturday()
{
// The year starts on a Monday: day 5 is the first Saturday, day 6 the first Sunday; 11:00 is the eleventh hour of the day.
IReadOnlyList<double> occupancy = ExampleNonResidentialPresets.RetailValues.Loads.Single(l => l.Type == LoadType.Occupancy).Schedule.Values;
Assert.Equal(0.6, occupancy[11]);
Assert.Equal(1.0, occupancy[(5 * 24) + 11]);
Assert.Equal(0.4, occupancy[(6 * 24) + 11]);
Assert.Equal(0.0, occupancy[(6 * 24) + 9]);
}
[Fact]
public void CareSchedulesAreTheSameEveryDay()
{
IReadOnlyList<double> bedroom = ExampleNonResidentialPresets.CareBedroomValues.Loads.Single(l => l.Type == LoadType.Occupancy).Schedule.Values;
Assert.Equal(bedroom.Take(24), bedroom.Skip(5 * 24).Take(24));
Assert.Equal(1.0, bedroom[2]);
Assert.Equal(0.2, bedroom[12]);
}
[Fact]
public void VentilationIsGivenOnlyForTheKitchenAndTheOperatingTheatre()
{
Assert.Equal(
new[] { SpaceType.Kitchen, SpaceType.OperatingTheatre },
ExampleNonResidentialPresets.SpaceTypes.Where(t => ExampleNonResidentialPresets.ValuesOf(t).Loads.Any(l => l.Type == LoadType.Ventilation)));
}
private static IEnumerable<(LoadType Type, LoadBasis Basis, double Value, string Schedule, double AnnualFraction)> Describe(PresetValues values) =>
values.Loads.Select(l => (l.Type, l.Basis, l.Value, l.Schedule.Name, Math.Round(l.Schedule.Values.Sum(), 6)));
}
- [x] Step 3: Run the tests and see them fail
dotnet test tests/Lod.Core.Tests -c Release --filter "FullyQualifiedName~Lod.Core.Tests.Programs.ExampleNonResidentialPresetsTests"
Expected: the build fails because the space types and the presets class do not exist yet, with these errors for the net8.0 target (in this order):
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(14,20): error CS0117: 'SpaceType' does not contain a definition for 'Office'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(27,20): error CS0117: 'SpaceType' does not contain a definition for 'Retail'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(34,20): error CS0117: 'SpaceType' does not contain a definition for 'Mall'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(40,20): error CS0117: 'SpaceType' does not contain a definition for 'Kitchen'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(49,20): error CS0117: 'SpaceType' does not contain a definition for 'Dining'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(56,20): error CS0117: 'SpaceType' does not contain a definition for 'OperatingTheatre'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(64,20): error CS0117: 'SpaceType' does not contain a definition for 'CleanCorridor'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(69,20): error CS0117: 'SpaceType' does not contain a definition for 'DirtyCorridor'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(74,20): error CS0117: 'SpaceType' does not contain a definition for 'ClinicalSupport'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(81,20): error CS0117: 'SpaceType' does not contain a definition for 'CareBedroom'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(88,20): error CS0117: 'SpaceType' does not contain a definition for 'CareCommunal'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(101,20): error CS0117: 'SpaceType' does not contain a definition for 'ActivityHall'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(117,52): error CS0117: 'SpaceType' does not contain a definition for 'Office'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(119,52): error CS0117: 'SpaceType' does not contain a definition for 'Retail'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(120,50): error CS0117: 'SpaceType' does not contain a definition for 'Mall'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(121,53): error CS0117: 'SpaceType' does not contain a definition for 'Kitchen'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(122,52): error CS0117: 'SpaceType' does not contain a definition for 'Dining'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(123,63): error CS0117: 'SpaceType' does not contain a definition for 'OperatingTheatre'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(124,60): error CS0117: 'SpaceType' does not contain a definition for 'CleanCorridor'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(125,60): error CS0117: 'SpaceType' does not contain a definition for 'DirtyCorridor'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(126,62): error CS0117: 'SpaceType' does not contain a definition for 'ClinicalSupport'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(127,58): error CS0117: 'SpaceType' does not contain a definition for 'CareBedroom'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(128,59): error CS0117: 'SpaceType' does not contain a definition for 'CareCommunal'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(130,59): error CS0117: 'SpaceType' does not contain a definition for 'ActivityHall'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(151,31): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(165,31): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(166,32): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(185,58): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(188,51): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(196,43): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(207,41): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(218,31): error CS0117: 'SpaceType' does not contain a definition for 'Kitchen'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(218,50): error CS0117: 'SpaceType' does not contain a definition for 'OperatingTheatre'
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(219,13): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
tests\Lod.Core.Tests\Programs\ExampleNonResidentialPresetsTests.cs(219,64): error CS0103: The name 'ExampleNonResidentialPresets' does not exist in the current context
- [x] Step 4: Implement — replace
src/Lod.Core/Programs/ZoneProgram.cs(full file)
Only SpaceType changes: the summaries of the enum, Core, Lobby, and Service, and the twelve values appended after Mixed. HeatingCooling, Thermostat, and ZoneProgram are unchanged.
using System;
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Common;
using Lod.Core.Loads;
using Lod.Core.Schedules;
namespace Lod.Core.Programs;
/// <summary>
/// Semantic category of a space (spec §7). Rooms are never modelled (D-009); a non-residential zone is one department (D-097). Values are only appended,
/// so existing values keep their order (ADR-014).
/// </summary>
public enum SpaceType
{
/// <summary>One dwelling unit; never subdivided.</summary>
DwellingUnit,
/// <summary>Circulation corridor.</summary>
Corridor,
/// <summary>Stair.</summary>
Stair,
/// <summary>Service or circulation core, such as an office service core with lifts, stairs, toilets, and risers.</summary>
Core,
/// <summary>Entrance lobby or public foyer.</summary>
Lobby,
/// <summary>Service space, such as the support rooms of a foyer: changing rooms, stores, toilets.</summary>
Service,
/// <summary>Mechanical space.</summary>
Mechanical,
/// <summary>Anything else.</summary>
Other,
/// <summary>A simplified zone combining several space types.</summary>
Mixed,
/// <summary>An office work area (ADR-014).</summary>
Office,
/// <summary>The shops of one mall wing, or one anchor store (ADR-014).</summary>
Retail,
/// <summary>The public mall route of a shopping centre (ADR-014).</summary>
Mall,
/// <summary>A kitchen with its servery, one food-service department (ADR-014).</summary>
Kitchen,
/// <summary>A dining field (ADR-014).</summary>
Dining,
/// <summary>A bank of operating theatres (ADR-014).</summary>
OperatingTheatre,
/// <summary>The clean route of an operating suite (ADR-014).</summary>
CleanCorridor,
/// <summary>The dirty (disposal) route of an operating suite (ADR-014).</summary>
DirtyCorridor,
/// <summary>The support rooms of an operating suite: scrub, anaesthesia, stores, recovery, sluice (ADR-014).</summary>
ClinicalSupport,
/// <summary>A group of care bedrooms of an elder-care home (ADR-014).</summary>
CareBedroom,
/// <summary>The communal and service hub of an elder-care home (ADR-014).</summary>
CareCommunal,
/// <summary>An activity hall for sport or community use (ADR-014).</summary>
ActivityHall,
}
/// <summary>Heating and cooling setpoints of a conditioned zone.</summary>
public sealed class Thermostat
{
private Thermostat(Schedule heatingSetpoint, Schedule coolingSetpoint)
{
HeatingSetpoint = heatingSetpoint;
CoolingSetpoint = coolingSetpoint;
}
/// <summary>Heating setpoint, °C.</summary>
public Schedule HeatingSetpoint { get; }
/// <summary>Cooling setpoint, °C.</summary>
public Schedule CoolingSetpoint { get; }
/// <summary>Creates a thermostat.</summary>
/// <param name="heatingSetpoint">A temperature schedule.</param>
/// <param name="coolingSetpoint">A temperature schedule.</param>
/// <returns>The thermostat or validation errors.</returns>
public static Result<Thermostat> Create(Schedule heatingSetpoint, Schedule coolingSetpoint)
{
Diagnostic[] diagnostics = new[] { heatingSetpoint, coolingSetpoint }
.Where(s => s.Kind != ScheduleKind.Temperature)
.Select(s => Diagnostic.Error(DiagnosticCodes.ScheduleKindMismatch, "Setpoints need temperature schedules.", s.Name))
.ToArray();
return Result.FromDiagnostics(() => new Thermostat(heatingSetpoint, coolingSetpoint), diagnostics);
}
}
/// <summary>
/// The energy-relevant program of a zone: its loads and, for conditioned zones, its thermostat. Conditioning comes from the program preset (D-038).
/// </summary>
public sealed class ZoneProgram
{
private ZoneProgram(LoadDefinition[] loads, Thermostat? thermostat)
{
Loads = Array.AsReadOnly(loads);
Thermostat = thermostat;
}
/// <summary>Loads ordered by <see cref="LoadType"/>, then <see cref="LoadBasis"/>; at most one per type and basis (D-047).</summary>
public IReadOnlyList<LoadDefinition> Loads { get; }
/// <summary>Setpoints of a conditioned zone; <c>null</c> for an unconditioned zone.</summary>
public Thermostat? Thermostat { get; }
/// <summary>Whether the zone is conditioned.</summary>
public bool IsConditioned => Thermostat is not null;
/// <summary>The load of a type expressed in a basis, or <c>null</c> when the program has none.</summary>
/// <param name="type">Load type.</param>
/// <param name="basis">Load basis.</param>
/// <returns>The load or <c>null</c>.</returns>
public LoadDefinition? Find(LoadType type, LoadBasis basis) => Loads.FirstOrDefault(l => l.Type == type && l.Basis == basis);
/// <summary>
/// Creates a program. A load type may appear once per basis, e.g. ventilation per person plus per floor area; its components add up (D-047).
/// </summary>
/// <param name="loads">Loads; at most one per type and basis; per-person loads need an occupancy load.</param>
/// <param name="thermostat">Setpoints for a conditioned zone, or <c>null</c> for an unconditioned zone.</param>
/// <returns>The program or validation errors.</returns>
public static Result<ZoneProgram> Create(IEnumerable<LoadDefinition> loads, Thermostat? thermostat)
{
LoadDefinition[] ordered = loads.OrderBy(l => l.Type).ThenBy(l => l.Basis).ToArray();
var diagnostics = new List<Diagnostic>();
foreach (IGrouping<(LoadType Type, LoadBasis Basis), LoadDefinition> group in ordered.GroupBy(l => (l.Type, l.Basis)).Where(g => g.Count() > 1))
{
diagnostics.Add(Diagnostic.Error(DiagnosticCodes.DuplicateLoadType, $"Load type {group.Key.Type} {group.Key.Basis} appears {group.Count()} times."));
}
if (ordered.Any(l => l.Basis == LoadBasis.PerPerson) && ordered.All(l => l.Type != LoadType.Occupancy))
{
diagnostics.Add(Diagnostic.Error(DiagnosticCodes.PerPersonWithoutOccupancy, "Per-person loads need an occupancy load in the same program."));
}
return Result.FromDiagnostics(() => new ZoneProgram(ordered, thermostat), diagnostics);
}
}
- [x] Step 5: Implement — create
src/Lod.Core/Programs/ExampleNonResidentialPresets.cs(full file)
using System;
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Loads;
using Lod.Core.Schedules;
namespace Lod.Core.Programs;
/// <summary>
/// Illustrative non-residential presets (ADR-014, D-094): one per space type of the four-source families' departments (D-097). The values are
/// plausible round-number choices for development and tests; they are NOT taken from the DOE prototypes, any standard, or measured data (D-023).
/// Every preset is conditioned. Sourced presets are a separate research input (see docs/research/program-presets.md).
/// </summary>
/// <remarks>
/// Schedules are fractions per hour of the day; the year starts on a Monday. Monday to Friday use the weekday profile and Saturday and Sunday the
/// weekend profile, except the retail schedules, which have their own Saturday and Sunday profiles because shops trade longest on Saturday.
/// </remarks>
public static class ExampleNonResidentialPresets
{
private const DayOfWeek FirstDayOfYear = DayOfWeek.Monday;
private const int HoursPerDay = 24;
private const int DaysPerWeek = 7;
private static readonly double[] OfficeOccupancyWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.5, 0.95, 0.95, 0.95,
0.5, 0.95, 0.95, 0.95, 0.95, 0.5, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] OfficeLightingWeekday =
{
0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.3, 0.8, 0.9, 0.9, 0.9,
0.8, 0.9, 0.9, 0.9, 0.9, 0.6, 0.3, 0.2, 0.05, 0.05, 0.05, 0.05,
};
private static readonly double[] OfficeEquipmentWeekday =
{
0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.4, 0.7, 0.9, 0.9, 0.9,
0.7, 0.9, 0.9, 0.9, 0.9, 0.6, 0.4, 0.3, 0.2, 0.2, 0.2, 0.2,
};
private static readonly double[] KitchenStaffWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.8, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] KitchenHoursWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] KitchenElectricWeekday =
{
0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.4, 0.6, 0.6, 0.6, 0.6, 0.9,
1.0, 0.9, 0.6, 0.4, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2,
};
private static readonly double[] KitchenGasWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.3, 0.3, 0.6, 1.0,
1.0, 0.6, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] DiningOccupancyWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.2, 0.1, 0.1, 0.4,
1.0, 0.8, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] DiningHoursWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] TheatreSessionsWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] TheatreServicesWeekday =
{
0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.6, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.6, 0.2, 0.2, 0.2, 0.2, 0.2,
};
private static readonly double[] TheatreVentilationWeekday =
{
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, 0.5,
};
private static readonly double[] CareBedroomOccupancyWeekday =
{
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.5, 0.3, 0.3, 0.3,
0.2, 0.4, 0.4, 0.4, 0.3, 0.2, 0.4, 0.6, 0.8, 1.0, 1.0, 1.0,
};
private static readonly double[] CareBedroomLightingWeekday =
{
0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.3, 0.6, 0.4, 0.1, 0.1, 0.1,
0.1, 0.1, 0.1, 0.1, 0.2, 0.4, 0.6, 0.8, 0.8, 0.6, 0.3, 0.1,
};
private static readonly double[] CareCommunalOccupancyWeekday =
{
0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.3, 0.9, 0.6, 0.7, 0.7,
1.0, 0.6, 0.7, 0.7, 0.7, 1.0, 0.8, 0.6, 0.4, 0.2, 0.05, 0.05,
};
private static readonly double[] CareCommunalHoursWeekday =
{
0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.2, 0.2,
};
private static readonly double[] CommunityOccupancyWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.2, 0.2,
0.2, 0.2, 0.2, 0.2, 0.2, 0.6, 0.9, 0.9, 0.9, 0.6, 0.2, 0.0,
};
private static readonly double[] CommunityOccupancyWeekend =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.8, 0.8, 0.8,
0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.4, 0.0, 0.0, 0.0,
};
private static readonly double[] CommunityHoursWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0,
};
private static readonly double[] CommunityHoursWeekend =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0,
};
private static readonly double[] RetailOccupancyWeekday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.5, 0.6,
0.8, 0.8, 0.6, 0.6, 0.7, 0.8, 0.7, 0.4, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] RetailOccupancySaturday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.8, 1.0,
1.0, 1.0, 1.0, 1.0, 0.9, 0.8, 0.6, 0.3, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] RetailOccupancySunday =
{
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4,
0.6, 0.7, 0.7, 0.6, 0.4, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
};
private static readonly double[] RetailLightingWeekday =
{
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.5, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.1, 0.1, 0.1,
};
private static readonly double[] RetailLightingSunday =
{
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.5, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
};
private static readonly double[] RetailEquipmentWeekday =
{
0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.6, 0.9, 0.9, 0.9,
0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.6, 0.3, 0.3, 0.3,
};
private static readonly double[] RetailEquipmentSunday =
{
0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.6, 0.9,
0.9, 0.9, 0.9, 0.9, 0.9, 0.6, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3,
};
private static readonly Schedule OfficeOccupancy = Profile("Example Office Occupancy", OfficeOccupancyWeekday, Flat(0.0));
private static readonly Schedule OfficeLighting = Profile("Example Office Lighting", OfficeLightingWeekday, Flat(0.05));
private static readonly Schedule OfficeEquipment = Profile("Example Office Equipment", OfficeEquipmentWeekday, Flat(0.2));
private static readonly Schedule KitchenStaff = Profile("Example Kitchen Staff", KitchenStaffWeekday, Flat(0.0));
private static readonly Schedule KitchenHours = Profile("Example Kitchen Hours", KitchenHoursWeekday, Flat(0.0));
private static readonly Schedule KitchenElectric = Profile("Example Kitchen Electric", KitchenElectricWeekday, Flat(0.2));
private static readonly Schedule KitchenGas = Profile("Example Kitchen Gas", KitchenGasWeekday, Flat(0.0));
private static readonly Schedule DiningOccupancy = Profile("Example Dining Occupancy", DiningOccupancyWeekday, Flat(0.0));
private static readonly Schedule DiningHours = Profile("Example Dining Hours", DiningHoursWeekday, Flat(0.0));
private static readonly Schedule TheatreSessions = Profile("Example Theatre Sessions", TheatreSessionsWeekday, Flat(0.0));
private static readonly Schedule TheatreServices = Profile("Example Theatre Services", TheatreServicesWeekday, Flat(0.2));
private static readonly Schedule TheatreVentilation = Profile("Example Theatre Ventilation", TheatreVentilationWeekday, Flat(0.5));
private static readonly Schedule CareBedroomOccupancy = Profile("Example Care Bedroom Occupancy", CareBedroomOccupancyWeekday, CareBedroomOccupancyWeekday);
private static readonly Schedule CareBedroomLighting = Profile("Example Care Bedroom Lighting", CareBedroomLightingWeekday, CareBedroomLightingWeekday);
private static readonly Schedule CareCommunalOccupancy = Profile("Example Care Communal Occupancy", CareCommunalOccupancyWeekday, CareCommunalOccupancyWeekday);
private static readonly Schedule CareCommunalHours = Profile("Example Care Communal Hours", CareCommunalHoursWeekday, CareCommunalHoursWeekday);
private static readonly Schedule CommunityOccupancy = Profile("Example Community Occupancy", CommunityOccupancyWeekday, CommunityOccupancyWeekend);
private static readonly Schedule CommunityHours = Profile("Example Community Hours", CommunityHoursWeekday, CommunityHoursWeekend);
private static readonly Schedule RetailOccupancy = Weekly("Example Retail Occupancy", RetailOccupancyWeekday, RetailOccupancySaturday, RetailOccupancySunday);
private static readonly Schedule RetailLighting = Weekly("Example Retail Lighting", RetailLightingWeekday, RetailLightingWeekday, RetailLightingSunday);
private static readonly Schedule RetailEquipment = Weekly("Example Retail Equipment", RetailEquipmentWeekday, RetailEquipmentWeekday, RetailEquipmentSunday);
private static readonly Schedule AlwaysOn = Constant("Example Always On", 1.0);
private static readonly Schedule CareBedroomEquipment = Constant("Example Care Bedroom Equipment", 0.5);
private static readonly SpaceType[] Types =
{
SpaceType.Office,
SpaceType.Core,
SpaceType.Retail,
SpaceType.Mall,
SpaceType.Kitchen,
SpaceType.Dining,
SpaceType.OperatingTheatre,
SpaceType.CleanCorridor,
SpaceType.DirtyCorridor,
SpaceType.ClinicalSupport,
SpaceType.CareBedroom,
SpaceType.CareCommunal,
SpaceType.Lobby,
SpaceType.ActivityHall,
SpaceType.Service,
};
/// <summary>Values of the office preset: an office work area. Conditioned, 21.0 / 24.0 °C, WWR 0.4.</summary>
public static PresetValues OfficeValues { get; } = Values(
"Example Office",
SpaceType.Office,
0.4,
21.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, OfficeOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, OfficeLighting),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 10.0, OfficeEquipment),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the core preset: an office service core (lifts, stairs, toilets, risers); WWR 0 because a core is enclosed by shafts and service rooms. Conditioned, 21.0 / 24.0 °C, WWR 0.0.</summary>
public static PresetValues CoreValues { get; } = Values(
"Example Core",
SpaceType.Core,
0.0,
21.0,
24.0,
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 5.0, OfficeLighting),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 3.0, OfficeEquipment),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the retail preset: the shops of one mall wing or an anchor store. Conditioned, 20.0 / 24.0 °C, WWR 0.3.</summary>
public static PresetValues RetailValues { get; } = Values(
"Example Retail",
SpaceType.Retail,
0.3,
20.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.2, RetailOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 15.0, RetailLighting),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 5.0, RetailEquipment),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.5, AlwaysOn));
/// <summary>Values of the mall preset: the public mall route. Conditioned, 20.0 / 24.0 °C, WWR 0.2.</summary>
public static PresetValues MallValues { get; } = Values(
"Example Mall",
SpaceType.Mall,
0.2,
20.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, RetailOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, RetailLighting),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.8, AlwaysOn));
/// <summary>Values of the kitchen preset: a kitchen with its servery: cooking load in electric and gas equipment and hood extract ventilation. Conditioned, 18.0 / 26.0 °C, WWR 0.1.</summary>
public static PresetValues KitchenValues { get; } = Values(
"Example Kitchen",
SpaceType.Kitchen,
0.1,
18.0,
26.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.05, KitchenStaff),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, KitchenHours),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 30.0, KitchenElectric),
new PresetLoadValue(LoadType.GasEquipment, LoadBasis.PerFloorArea, 40.0, KitchenGas),
new PresetLoadValue(LoadType.Ventilation, LoadBasis.AirChangesPerHour, 15.0, KitchenHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the dining preset: a dining field, densely occupied around lunch. Conditioned, 21.0 / 24.0 °C, WWR 0.4.</summary>
public static PresetValues DiningValues { get; } = Values(
"Example Dining",
SpaceType.Dining,
0.4,
21.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.6, DiningOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, DiningHours),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 2.0, DiningHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the operating theatre preset: a bank of operating theatres: high equipment, ventilation with a night setback, a tight setpoint band; WWR 0. Conditioned, 20.0 / 22.0 °C, WWR 0.0.</summary>
public static PresetValues OperatingTheatreValues { get; } = Values(
"Example Operating Theatre",
SpaceType.OperatingTheatre,
0.0,
20.0,
22.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, TheatreSessions),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 25.0, TheatreServices),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 30.0, TheatreServices),
new PresetLoadValue(LoadType.Ventilation, LoadBasis.AirChangesPerHour, 20.0, TheatreVentilation),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.1, AlwaysOn));
/// <summary>Values of the clean corridor preset: the clean route of an operating suite. Conditioned, 20.0 / 24.0 °C, WWR 0.1.</summary>
public static PresetValues CleanCorridorValues { get; } = Values(
"Example Clean Corridor",
SpaceType.CleanCorridor,
0.1,
20.0,
24.0,
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 8.0, AlwaysOn),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the dirty corridor preset: the dirty route of an operating suite; the same values as the clean corridor. Conditioned, 20.0 / 24.0 °C, WWR 0.1.</summary>
public static PresetValues DirtyCorridorValues { get; } = Values(
"Example Dirty Corridor",
SpaceType.DirtyCorridor,
0.1,
20.0,
24.0,
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 8.0, AlwaysOn),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the clinical support preset: the support rooms of an operating suite. Conditioned, 21.0 / 24.0 °C, WWR 0.2.</summary>
public static PresetValues ClinicalSupportValues { get; } = Values(
"Example Clinical Support",
SpaceType.ClinicalSupport,
0.2,
21.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.05, TheatreSessions),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, TheatreServices),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 10.0, TheatreServices),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the care bedroom preset: a group of care bedrooms, occupied day and night every day. Conditioned, 22.0 / 25.0 °C, WWR 0.3.</summary>
public static PresetValues CareBedroomValues { get; } = Values(
"Example Care Bedroom",
SpaceType.CareBedroom,
0.3,
22.0,
25.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.04, CareBedroomOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 6.0, CareBedroomLighting),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 3.0, CareBedroomEquipment),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the care communal preset: the communal and service hub of a care home. Conditioned, 22.0 / 25.0 °C, WWR 0.4.</summary>
public static PresetValues CareCommunalValues { get; } = Values(
"Example Care Communal",
SpaceType.CareCommunal,
0.4,
22.0,
25.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.15, CareCommunalOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, CareCommunalHours),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 5.0, CareCommunalHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the lobby preset: a public foyer. Conditioned, 20.0 / 24.0 °C, WWR 0.5.</summary>
public static PresetValues LobbyValues { get; } = Values(
"Example Lobby",
SpaceType.Lobby,
0.5,
20.0,
24.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, CommunityOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 10.0, CommunityHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.6, AlwaysOn));
/// <summary>Values of the activity hall preset: an activity hall, used in the evenings and at weekends. Conditioned, 18.0 / 25.0 °C, WWR 0.2.</summary>
public static PresetValues ActivityHallValues { get; } = Values(
"Example Activity Hall",
SpaceType.ActivityHall,
0.2,
18.0,
25.0,
new PresetLoadValue(LoadType.Occupancy, LoadBasis.PerFloorArea, 0.1, CommunityOccupancy),
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 12.0, CommunityHours),
new PresetLoadValue(LoadType.ElectricEquipment, LoadBasis.PerFloorArea, 2.0, CommunityHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Values of the service preset: the support rooms of a foyer (changing rooms, stores, toilets). Conditioned, 18.0 / 26.0 °C, WWR 0.1.</summary>
public static PresetValues ServiceValues { get; } = Values(
"Example Service",
SpaceType.Service,
0.1,
18.0,
26.0,
new PresetLoadValue(LoadType.Lighting, LoadBasis.PerFloorArea, 6.0, CommunityHours),
new PresetLoadValue(LoadType.Infiltration, LoadBasis.AirChangesPerHour, 0.3, AlwaysOn));
/// <summary>Office preset, built from <see cref="OfficeValues"/>.</summary>
public static ProgramPreset Office { get; } = OfficeValues.ToPreset().Value;
/// <summary>Core preset, built from <see cref="CoreValues"/>.</summary>
public static ProgramPreset Core { get; } = CoreValues.ToPreset().Value;
/// <summary>Retail preset, built from <see cref="RetailValues"/>.</summary>
public static ProgramPreset Retail { get; } = RetailValues.ToPreset().Value;
/// <summary>Mall preset, built from <see cref="MallValues"/>.</summary>
public static ProgramPreset Mall { get; } = MallValues.ToPreset().Value;
/// <summary>Kitchen preset, built from <see cref="KitchenValues"/>.</summary>
public static ProgramPreset Kitchen { get; } = KitchenValues.ToPreset().Value;
/// <summary>Dining preset, built from <see cref="DiningValues"/>.</summary>
public static ProgramPreset Dining { get; } = DiningValues.ToPreset().Value;
/// <summary>Operating Theatre preset, built from <see cref="OperatingTheatreValues"/>.</summary>
public static ProgramPreset OperatingTheatre { get; } = OperatingTheatreValues.ToPreset().Value;
/// <summary>Clean Corridor preset, built from <see cref="CleanCorridorValues"/>.</summary>
public static ProgramPreset CleanCorridor { get; } = CleanCorridorValues.ToPreset().Value;
/// <summary>Dirty Corridor preset, built from <see cref="DirtyCorridorValues"/>.</summary>
public static ProgramPreset DirtyCorridor { get; } = DirtyCorridorValues.ToPreset().Value;
/// <summary>Clinical Support preset, built from <see cref="ClinicalSupportValues"/>.</summary>
public static ProgramPreset ClinicalSupport { get; } = ClinicalSupportValues.ToPreset().Value;
/// <summary>Care Bedroom preset, built from <see cref="CareBedroomValues"/>.</summary>
public static ProgramPreset CareBedroom { get; } = CareBedroomValues.ToPreset().Value;
/// <summary>Care Communal preset, built from <see cref="CareCommunalValues"/>.</summary>
public static ProgramPreset CareCommunal { get; } = CareCommunalValues.ToPreset().Value;
/// <summary>Lobby preset, built from <see cref="LobbyValues"/>.</summary>
public static ProgramPreset Lobby { get; } = LobbyValues.ToPreset().Value;
/// <summary>Activity Hall preset, built from <see cref="ActivityHallValues"/>.</summary>
public static ProgramPreset ActivityHall { get; } = ActivityHallValues.ToPreset().Value;
/// <summary>Service preset, built from <see cref="ServiceValues"/>.</summary>
public static ProgramPreset Service { get; } = ServiceValues.ToPreset().Value;
/// <summary>The space types that have a preset here, in the order of ADR-014.</summary>
public static IReadOnlyList<SpaceType> SpaceTypes { get; } = Array.AsReadOnly(Types);
/// <summary>The preset values of a space type.</summary>
/// <param name="spaceType">One of <see cref="SpaceTypes"/>.</param>
/// <returns>The values.</returns>
/// <exception cref="ArgumentOutOfRangeException">The space type has no non-residential example preset.</exception>
public static PresetValues ValuesOf(SpaceType spaceType) => spaceType switch
{
SpaceType.Office => OfficeValues,
SpaceType.Core => CoreValues,
SpaceType.Retail => RetailValues,
SpaceType.Mall => MallValues,
SpaceType.Kitchen => KitchenValues,
SpaceType.Dining => DiningValues,
SpaceType.OperatingTheatre => OperatingTheatreValues,
SpaceType.CleanCorridor => CleanCorridorValues,
SpaceType.DirtyCorridor => DirtyCorridorValues,
SpaceType.ClinicalSupport => ClinicalSupportValues,
SpaceType.CareBedroom => CareBedroomValues,
SpaceType.CareCommunal => CareCommunalValues,
SpaceType.Lobby => LobbyValues,
SpaceType.ActivityHall => ActivityHallValues,
SpaceType.Service => ServiceValues,
_ => throw new ArgumentOutOfRangeException(nameof(spaceType), spaceType, "No non-residential example preset for this space type."),
};
/// <summary>The preset of a space type.</summary>
/// <param name="spaceType">One of <see cref="SpaceTypes"/>.</param>
/// <returns>The preset.</returns>
/// <exception cref="ArgumentOutOfRangeException">The space type has no non-residential example preset.</exception>
public static ProgramPreset PresetOf(SpaceType spaceType) => spaceType switch
{
SpaceType.Office => Office,
SpaceType.Core => Core,
SpaceType.Retail => Retail,
SpaceType.Mall => Mall,
SpaceType.Kitchen => Kitchen,
SpaceType.Dining => Dining,
SpaceType.OperatingTheatre => OperatingTheatre,
SpaceType.CleanCorridor => CleanCorridor,
SpaceType.DirtyCorridor => DirtyCorridor,
SpaceType.ClinicalSupport => ClinicalSupport,
SpaceType.CareBedroom => CareBedroom,
SpaceType.CareCommunal => CareCommunal,
SpaceType.Lobby => Lobby,
SpaceType.ActivityHall => ActivityHall,
SpaceType.Service => Service,
_ => throw new ArgumentOutOfRangeException(nameof(spaceType), spaceType, "No non-residential example preset for this space type."),
};
private static PresetValues Values(string name, SpaceType spaceType, double windowToWallRatio, double heating, double cooling, params PresetLoadValue[] loads) =>
new(name, spaceType, windowToWallRatio, Conditioned: true, heating, cooling, loads);
private static double[] Flat(double value) => Enumerable.Repeat(value, HoursPerDay).ToArray();
private static Schedule Constant(string name, double value) =>
Schedule.Constant(name, ScheduleKind.Fraction, value).Value;
private static Schedule Profile(string name, double[] weekday, double[] weekend) =>
Schedule.FromDailyProfiles(name, ScheduleKind.Fraction, weekday, weekend, FirstDayOfYear).Value;
/// <summary>A year of fractions with one profile for Monday to Friday, one for Saturday, and one for Sunday.</summary>
private static Schedule Weekly(string name, double[] weekday, double[] saturday, double[] sunday)
{
var values = new double[Schedule.HoursPerYear];
for (int hour = 0; hour < values.Length; hour++)
{
var day = (DayOfWeek)(((int)FirstDayOfYear + (hour / HoursPerDay)) % DaysPerWeek);
double[] profile = day switch
{
DayOfWeek.Saturday => saturday,
DayOfWeek.Sunday => sunday,
_ => weekday,
};
values[hour] = profile[hour % HoursPerDay];
}
return Schedule.Create(name, ScheduleKind.Fraction, values).Value;
}
}
- [x] Step 6: Run the tests and see them pass
dotnet test tests/Lod.Core.Tests -c Release --filter "FullyQualifiedName~Lod.Core.Tests.Programs.ExampleNonResidentialPresetsTests"
dotnet build BEMGen.sln -c Release
dotnet test BEMGen.sln -c Release
git status --short --untracked-files=all
Expected: Passed! - Failed: 0, Passed: 35, Skipped: 0, Total: 35, Duration: … - Lod.Core.Tests.dll (net8.0); Build succeeded., 0 Warning(s), 0 Error(s); 193 core, 181 generators, and 603 integration tests passed on net8.0; git status lists M src/Lod.Core/Programs/ZoneProgram.cs and the two new files, and no *.received.txt: appending enum values changes no existing preset, plan, or snapshot.
- [x] Step 7: Commit
git add src/Lod.Core/Programs/ZoneProgram.cs src/Lod.Core/Programs/ExampleNonResidentialPresets.cs tests/Lod.Core.Tests/Programs/ExampleNonResidentialPresetsTests.cs
git commit -m "feature(programs): add non-residential space types and example presets"
Task 6: Default preset components for the non-residential space types, with icons¶
Files:
- Modify: src/Lod.Grasshopper/Components/PresetComponents.cs (S4.1 file), src/Lod.Grasshopper/Goo/BemGoo.cs (S2 file, last changed in S4.2), scripts/make_icons.py (S4.1 file, last changed in S8.3)
- Create (generated): src/Lod.Grasshopper/Icons/OfficePresetComponent.png, CorePresetComponent.png, RetailPresetComponent.png, MallPresetComponent.png, KitchenPresetComponent.png, DiningPresetComponent.png, OperatingTheatrePresetComponent.png, CleanCorridorPresetComponent.png, DirtyCorridorPresetComponent.png, ClinicalSupportPresetComponent.png, CareBedroomPresetComponent.png, CareCommunalPresetComponent.png, LobbyPresetComponent.png, ActivityHallPresetComponent.png, ServicePresetComponent.png
Interfaces:
- Consumes: ExampleNonResidentialPresets (Task 5); the S4.1 base DefaultPresetComponent (panel 1 Program, one input per field of PresetValues with its default, the label and unit of every LoadType and LoadBasis, which already cover gas equipment in W/m² and ventilation in 1/h, so no mapping is extended; the remark "Illustrative values; not sourced from DOE prototypes or standards."); PreviewGoo.SpaceTypeColour (private); IconLibrary.For and the Icons\*.png embedding of Lod.Grasshopper.csproj (S4.1, unchanged).
- Produces: fifteen sealed components after Stair Preset, each with the GUID of the Global Constraints, the nickname Office, Core, Retail, Mall, Kitchen (Kitch), Dining (Dine), Operating Theatre (OT), Clean Corridor (Clean), Dirty Corridor (Dirty), Clinical Support (ClinS), Care Bedroom (CBed), Care Communal (CComm), Lobby, Activity Hall (Hall), and Service (Serv), and its Defaults from ExampleNonResidentialPresets; a preview colour for each of the twelve new space types (Lobby and Service keep the default colour); fifteen motifs in the Program orange of the residential presets (an office block with a grid of windows, a service core with two lift doors and an up and down arrow, a shopping bag, shop blocks along a white T of mall routes, a cooking pot, a plate between a fork and a knife, a medical cross, a corridor with a white route and one with a hatched route, shelves beside a small medical cross, a bed, a round table with four people, a glazed entrance front with an open door, a sports court with its centre line and circle, and three lockers), mapped after StairPresetComponent. 44 components, 6 parameters, and 51 icons.
- [x] Step 1: Replace
src/Lod.Grasshopper/Components/PresetComponents.cs(full file)
The fifteen classes are added at the end; the base class and the three residential components are unchanged.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Grasshopper.Kernel;
using Lod.Core.Loads;
using Lod.Core.Programs;
using Lod.Grasshopper.Goo;
using Lod.Grasshopper.Parameters;
namespace Lod.Grasshopper.Components;
/// <summary>
/// Shared inputs and solving of the default preset components (D-061): one component per space type whose inputs all default to its
/// <see cref="PresetValues"/>, so it works with nothing connected. Inputs: Name, WWR, Conditioned, Heating, Cooling, then a value and an optional
/// schedule per load of the defaults, in their order.
/// </summary>
public abstract class DefaultPresetComponent : GH_Component
{
private const string IllustrativeRemark = "Illustrative values; not sourced from DOE prototypes or standards.";
private const int FirstLoadInput = 5;
protected DefaultPresetComponent(string name, string nickname, string description)
: base(name, nickname, description, ComponentCategories.Category, ComponentCategories.Program)
{
}
protected override Bitmap? Icon => IconLibrary.For(this);
/// <summary>The default values; read while inputs are registered, so they must not depend on instance state.</summary>
protected abstract PresetValues Defaults { get; }
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
PresetValues defaults = Defaults;
pManager.AddTextParameter("Name", "N", "Preset name; descriptive only (reports, provenance, messages), never used for matching.", GH_ParamAccess.item, defaults.Name);
pManager.AddNumberParameter("WWR", "WWR", "Window-to-wall ratio in [0, 1) used by the plan generator for every exterior wall.", GH_ParamAccess.item, defaults.WindowToWallRatio);
pManager.AddBooleanParameter("Conditioned", "Cond", "Whether the space is conditioned (D-038); the setpoints are ignored when it is not.", GH_ParamAccess.item, defaults.Conditioned);
pManager.AddNumberParameter("Heating", "H", "Constant heating setpoint, °C.", GH_ParamAccess.item, defaults.HeatingSetpoint);
pManager.AddNumberParameter("Cooling", "C", "Constant cooling setpoint, °C.", GH_ParamAccess.item, defaults.CoolingSetpoint);
foreach (PresetLoadValue load in defaults.Loads)
{
(string label, string nickname) = LoadLabel(defaults.Loads, load);
pManager.AddNumberParameter(label, nickname, $"{label} design value, {Unit(load.Type, load.Basis)}.", GH_ParamAccess.item, load.Value);
int schedule = pManager.AddParameter(
new ScheduleParameter(),
$"{label} Schedule",
nickname + "S",
$"Optional fraction schedule of {label}; replaces the built-in schedule '{load.Schedule.Name}' when connected.",
GH_ParamAccess.item);
pManager[schedule].Optional = true;
}
}
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddParameter(new ProgramPresetParameter(), "Preset", "P", "The program preset.", GH_ParamAccess.item);
}
protected override void SolveInstance(IGH_DataAccess DA)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, IllustrativeRemark);
PresetValues defaults = Defaults;
string name = string.Empty;
double wwr = 0.0;
bool conditioned = false;
double heating = 0.0;
double cooling = 0.0;
if (!DA.GetData(0, ref name) || !DA.GetData(1, ref wwr) || !DA.GetData(2, ref conditioned) || !DA.GetData(3, ref heating) || !DA.GetData(4, ref cooling))
{
return;
}
var loads = new PresetLoadValue[defaults.Loads.Count];
for (int k = 0; k < loads.Length; k++)
{
double value = 0.0;
ScheduleGoo? schedule = null;
if (!DA.GetData(FirstLoadInput + (2 * k), ref value))
{
return;
}
DA.GetData(FirstLoadInput + (2 * k) + 1, ref schedule);
loads[k] = defaults.Loads[k] with { Value = value, Schedule = schedule?.Value ?? defaults.Loads[k].Schedule };
}
PresetValues values = defaults with
{
Name = name,
WindowToWallRatio = wwr,
Conditioned = conditioned,
HeatingSetpoint = heating,
CoolingSetpoint = cooling,
Loads = loads,
};
ProgramPreset? preset = ComponentSupport.Unwrap(this, values.ToPreset());
if (preset is not null)
{
DA.SetData(0, new ProgramPresetGoo(preset));
}
}
/// <summary>Input name and nickname of a load; the basis is added when the defaults hold the load type in more than one basis (D-047).</summary>
private static (string Label, string Nickname) LoadLabel(IReadOnlyList<PresetLoadValue> loads, PresetLoadValue load)
{
(string label, string nickname) = load.Type switch
{
LoadType.Occupancy => ("Occupancy", "Occ"),
LoadType.Lighting => ("Lighting", "Lt"),
LoadType.ElectricEquipment => ("Equipment", "Eq"),
LoadType.GasEquipment => ("Gas Equipment", "Gas"),
LoadType.DomesticHotWater => ("Hot Water", "DHW"),
LoadType.Ventilation => ("Ventilation", "Vent"),
_ => ("Infiltration", "Inf"),
};
return loads.Count(l => l.Type == load.Type) > 1
? ($"{label} {load.Basis}", nickname + string.Concat(load.Basis.ToString().Where(char.IsUpper)))
: (label, nickname);
}
/// <summary>Unit of a load value: the magnitude unit of its type (people, W, m³/h) per its basis (ADR-007).</summary>
private static string Unit(LoadType type, LoadBasis basis)
{
string magnitude = type switch
{
LoadType.Occupancy => "people",
LoadType.Lighting or LoadType.ElectricEquipment or LoadType.GasEquipment => "W",
_ => "m³/h",
};
return basis switch
{
LoadBasis.PerFloorArea => $"{magnitude}/m²",
LoadBasis.PerPerson => $"{magnitude}/person",
LoadBasis.PerExteriorWallArea => $"{magnitude}/m² of exterior wall",
LoadBasis.AirChangesPerHour => "1/h (air changes per hour)",
_ => magnitude,
};
}
}
public sealed class DwellingUnitPresetComponent : DefaultPresetComponent
{
public DwellingUnitPresetComponent()
: base("Dwelling Unit Preset", "Unit", "Program preset of a dwelling unit (one zone per unit). Every input defaults to the illustrative example values (D-061).")
{
}
public override Guid ComponentGuid => new("b491330c-bc92-42e7-b494-6946e66b599a");
protected override PresetValues Defaults => ExampleResidentialPresets.DwellingUnitValues;
}
public sealed class CorridorPresetComponent : DefaultPresetComponent
{
public CorridorPresetComponent()
: base("Corridor Preset", "Corr", "Program preset of a corridor. Every input defaults to the illustrative example values (D-061).")
{
}
public override Guid ComponentGuid => new("5947d3ea-25fe-4dc3-b388-384d8326826e");
protected override PresetValues Defaults => ExampleResidentialPresets.CorridorValues;
}
public sealed class StairPresetComponent : DefaultPresetComponent
{
public StairPresetComponent()
: base("Stair Preset", "Stair", "Program preset of a stair. Every input defaults to the illustrative example values (D-061).")
{
}
public override Guid ComponentGuid => new("717ccc25-1097-4b66-addc-d2fcdd4e385a");
protected override PresetValues Defaults => ExampleResidentialPresets.StairValues;
}
public sealed class OfficePresetComponent : DefaultPresetComponent
{
public OfficePresetComponent()
: base("Office Preset", "Office", "Program preset of an office work area (one zone per work-area department, D-097); space type Office. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("4160f791-07dc-4529-9986-1c9ba7d21a98");
protected override PresetValues Defaults => ExampleNonResidentialPresets.OfficeValues;
}
public sealed class CorePresetComponent : DefaultPresetComponent
{
public CorePresetComponent()
: base("Core Preset", "Core", "Program preset of an office service core: lifts, stairs, toilets, risers; space type Core. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("ca44b061-1ced-4ae3-afed-d806e6db2b7c");
protected override PresetValues Defaults => ExampleNonResidentialPresets.CoreValues;
}
public sealed class RetailPresetComponent : DefaultPresetComponent
{
public RetailPresetComponent()
: base("Retail Preset", "Retail", "Program preset of the shops of one mall wing or an anchor store; space type Retail. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("b760860d-05c9-4cbe-a22f-ddd2e38e4d64");
protected override PresetValues Defaults => ExampleNonResidentialPresets.RetailValues;
}
public sealed class MallPresetComponent : DefaultPresetComponent
{
public MallPresetComponent()
: base("Mall Preset", "Mall", "Program preset of the public mall route; space type Mall. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("dbd514d5-7553-4000-b10c-bca748e3b301");
protected override PresetValues Defaults => ExampleNonResidentialPresets.MallValues;
}
public sealed class KitchenPresetComponent : DefaultPresetComponent
{
public KitchenPresetComponent()
: base("Kitchen Preset", "Kitch", "Program preset of a kitchen with its servery, one food-service department; space type Kitchen. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("658e05dc-c935-4518-bc99-512546153e1e");
protected override PresetValues Defaults => ExampleNonResidentialPresets.KitchenValues;
}
public sealed class DiningPresetComponent : DefaultPresetComponent
{
public DiningPresetComponent()
: base("Dining Preset", "Dine", "Program preset of a dining field; space type Dining. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("c8b05fa2-330a-4e8a-acaa-e57ca57b4fd9");
protected override PresetValues Defaults => ExampleNonResidentialPresets.DiningValues;
}
public sealed class OperatingTheatrePresetComponent : DefaultPresetComponent
{
public OperatingTheatrePresetComponent()
: base("Operating Theatre Preset", "OT", "Program preset of a bank of operating theatres; space type OperatingTheatre. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("eb55b6d5-08f1-4a5e-9598-13951c34c2a4");
protected override PresetValues Defaults => ExampleNonResidentialPresets.OperatingTheatreValues;
}
public sealed class CleanCorridorPresetComponent : DefaultPresetComponent
{
public CleanCorridorPresetComponent()
: base("Clean Corridor Preset", "Clean", "Program preset of the clean route of an operating suite; space type CleanCorridor. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("c199a986-28b8-41c7-b260-5f4581b7362b");
protected override PresetValues Defaults => ExampleNonResidentialPresets.CleanCorridorValues;
}
public sealed class DirtyCorridorPresetComponent : DefaultPresetComponent
{
public DirtyCorridorPresetComponent()
: base("Dirty Corridor Preset", "Dirty", "Program preset of the dirty (disposal) route of an operating suite; space type DirtyCorridor. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("0e61c86e-5981-4a11-b922-52650757e4a9");
protected override PresetValues Defaults => ExampleNonResidentialPresets.DirtyCorridorValues;
}
public sealed class ClinicalSupportPresetComponent : DefaultPresetComponent
{
public ClinicalSupportPresetComponent()
: base("Clinical Support Preset", "ClinS", "Program preset of the support rooms of an operating suite; space type ClinicalSupport. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("274e0ac9-de95-4bd4-8028-25d74bfd1e4b");
protected override PresetValues Defaults => ExampleNonResidentialPresets.ClinicalSupportValues;
}
public sealed class CareBedroomPresetComponent : DefaultPresetComponent
{
public CareBedroomPresetComponent()
: base("Care Bedroom Preset", "CBed", "Program preset of a group of care bedrooms; space type CareBedroom. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("8447ea38-faec-4630-96dd-92569cd7d0d8");
protected override PresetValues Defaults => ExampleNonResidentialPresets.CareBedroomValues;
}
public sealed class CareCommunalPresetComponent : DefaultPresetComponent
{
public CareCommunalPresetComponent()
: base("Care Communal Preset", "CComm", "Program preset of the communal and service hub of a care home; space type CareCommunal. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("a3000f8c-633f-4a54-9fb8-1a4109e337bb");
protected override PresetValues Defaults => ExampleNonResidentialPresets.CareCommunalValues;
}
public sealed class LobbyPresetComponent : DefaultPresetComponent
{
public LobbyPresetComponent()
: base("Lobby Preset", "Lobby", "Program preset of a public foyer or entrance lobby; space type Lobby. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("395aebb3-3e49-4814-93ab-388da9de2360");
protected override PresetValues Defaults => ExampleNonResidentialPresets.LobbyValues;
}
public sealed class ActivityHallPresetComponent : DefaultPresetComponent
{
public ActivityHallPresetComponent()
: base("Activity Hall Preset", "Hall", "Program preset of an activity hall for sport or community use; space type ActivityHall. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("6bf00cd3-70b9-4337-8b80-70fc00d79666");
protected override PresetValues Defaults => ExampleNonResidentialPresets.ActivityHallValues;
}
public sealed class ServicePresetComponent : DefaultPresetComponent
{
public ServicePresetComponent()
: base("Service Preset", "Serv", "Program preset of service or support rooms: changing rooms, stores, toilets; space type Service. Every input defaults to the illustrative example values (ADR-014, D-061).")
{
}
public override Guid ComponentGuid => new("60251c8e-ad4b-4ef5-8f68-64f4c6bf6f8b");
protected override PresetValues Defaults => ExampleNonResidentialPresets.ServiceValues;
}
- [x] Step 2: Replace
src/Lod.Grasshopper/Goo/BemGoo.cs(full file)
Only the twelve colours are added to SpaceTypeColour.
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using Lod.Core.Loads;
using Lod.Core.Model;
using Lod.Core.Programs;
using Lod.Core.Schedules;
using Lod.Grasshopper.Convert;
using Rhino.Display;
using Rhino.Geometry;
using Surface = Lod.Core.Model.Surface;
namespace Lod.Grasshopper.Goo;
/// <summary>Grasshopper wrapper of an immutable pipeline object. Values are never modified, so duplicates share the value.</summary>
/// <typeparam name="T">Wrapped type.</typeparam>
public abstract class BemGoo<T> : GH_Goo<T>
where T : class
{
protected BemGoo(T value)
: base(value)
{
}
public override bool IsValid => Value is not null;
public override string TypeDescription => $"BEMGen {TypeName}";
}
public sealed class ScheduleGoo : BemGoo<Schedule>
{
public ScheduleGoo(Schedule value)
: base(value)
{
}
public override string TypeName => "Schedule";
public override IGH_Goo Duplicate() => new ScheduleGoo(Value);
public override string ToString() => $"Schedule {Value.Name} ({Value.Kind})";
}
public sealed class LoadGoo : BemGoo<LoadDefinition>
{
public LoadGoo(LoadDefinition value)
: base(value)
{
}
public override string TypeName => "Load";
public override IGH_Goo Duplicate() => new LoadGoo(Value);
public override string ToString() => $"Load {Value.Type} {Value.Value} {Value.Basis}";
}
public sealed class ProgramPresetGoo : BemGoo<ProgramPreset>
{
public ProgramPresetGoo(ProgramPreset value)
: base(value)
{
}
public override string TypeName => "Program Preset";
public override IGH_Goo Duplicate() => new ProgramPresetGoo(Value);
public override string ToString() => $"Preset {Value.Name} ({Value.SpaceType}, WWR {Value.WindowToWallRatio})";
}
/// <summary>
/// Wrapper with viewport preview: zones coloured by space type (a zone with several parts as one merged volume, D-071), storeys that are represented
/// but not modelled in transparent grey (D-071, D-075), windows in blue. The preview is moved by <see cref="PreviewOffset"/> (D-062); the wrapped
/// value keeps its true coordinates.
/// </summary>
/// <typeparam name="T">Wrapped type.</typeparam>
public abstract class PreviewGoo<T> : BemGoo<T>, IGH_PreviewData
where T : class
{
private static readonly Color WindowColour = Color.FromArgb(70, 140, 220);
private static readonly Color UnmodelledColour = Color.FromArgb(150, 150, 150);
private List<(Brep Brep, DisplayMaterial Material)>? _preview;
protected PreviewGoo(T value, Vector3d previewOffset)
: base(value)
{
PreviewOffset = previewOffset;
}
/// <summary>Translation of the viewport preview only, in model units (D-062).</summary>
public Vector3d PreviewOffset { get; }
public BoundingBox ClippingBox
{
get
{
BoundingBox box = BoundingBox.Empty;
foreach ((Brep brep, DisplayMaterial _) in Preview)
{
box.Union(brep.GetBoundingBox(false));
}
return box;
}
}
private List<(Brep Brep, DisplayMaterial Material)> Preview => _preview ??= BuildPreview();
public void DrawViewportMeshes(GH_PreviewMeshArgs args)
{
foreach ((Brep brep, DisplayMaterial material) in Preview)
{
args.Pipeline.DrawBrepShaded(brep, material);
}
}
public void DrawViewportWires(GH_PreviewWireArgs args)
{
foreach ((Brep brep, DisplayMaterial _) in Preview)
{
args.Pipeline.DrawBrepWires(brep, args.Color);
}
}
protected abstract IEnumerable<Zone> Zones { get; }
protected abstract IEnumerable<Surface> Surfaces { get; }
/// <summary>Parts of storeys that are represented but not modelled, drawn transparent grey; none unless a building says so.</summary>
protected virtual IEnumerable<ZonePart> UnmodelledParts => Enumerable.Empty<ZonePart>();
private List<(Brep, DisplayMaterial)> BuildPreview()
{
var items = new List<(Brep, DisplayMaterial)>();
foreach (Zone zone in Zones)
{
var material = new DisplayMaterial(SpaceTypeColour(zone.SpaceType), 0.6);
IEnumerable<Brep> volumes = zone.Parts.Count > 1 ? RhinoGeometry.MergedZoneBreps(zone) : RhinoGeometry.ZoneBreps(zone);
items.AddRange(volumes.Select(b => (b, material)));
}
var unmodelled = new DisplayMaterial(UnmodelledColour, 0.85);
items.AddRange(RhinoGeometry.PartBreps(UnmodelledParts).Select(b => (b, unmodelled)));
var glass = new DisplayMaterial(WindowColour, 0.2);
foreach (WallSurface wall in Surfaces.OfType<WallSurface>())
{
items.AddRange(RhinoGeometry.WindowBreps(wall).Select(b => (b, glass)));
}
if (!PreviewOffset.IsZero)
{
Transform move = Transform.Translation(PreviewOffset);
foreach ((Brep brep, DisplayMaterial _) in items)
{
brep.Transform(move);
}
}
return items;
}
private static Color SpaceTypeColour(SpaceType type) => type switch
{
SpaceType.DwellingUnit => Color.FromArgb(240, 200, 120),
SpaceType.Corridor => Color.FromArgb(180, 180, 180),
SpaceType.Stair => Color.FromArgb(200, 120, 120),
SpaceType.Core => Color.FromArgb(160, 120, 200),
SpaceType.Mixed => Color.FromArgb(120, 190, 160),
SpaceType.Office => Color.FromArgb(130, 170, 220),
SpaceType.Retail => Color.FromArgb(230, 150, 170),
SpaceType.Mall => Color.FromArgb(210, 210, 160),
SpaceType.Kitchen => Color.FromArgb(220, 130, 90),
SpaceType.Dining => Color.FromArgb(240, 220, 150),
SpaceType.OperatingTheatre => Color.FromArgb(110, 190, 200),
SpaceType.CleanCorridor => Color.FromArgb(200, 230, 230),
SpaceType.DirtyCorridor => Color.FromArgb(160, 140, 120),
SpaceType.ClinicalSupport => Color.FromArgb(150, 200, 180),
SpaceType.CareBedroom => Color.FromArgb(230, 180, 200),
SpaceType.CareCommunal => Color.FromArgb(200, 160, 220),
SpaceType.ActivityHall => Color.FromArgb(170, 210, 120),
_ => Color.FromArgb(150, 170, 200),
};
}
public sealed class PlanGoo : PreviewGoo<IGeneratedPlan>
{
public PlanGoo(IGeneratedPlan value, Vector3d previewOffset = default)
: base(value, previewOffset)
{
}
public override string TypeName => "Plan";
protected override IEnumerable<Zone> Zones => Value.Zones;
protected override IEnumerable<Surface> Surfaces => Value.Surfaces;
public override IGH_Goo Duplicate() => new PlanGoo(Value, PreviewOffset);
public override string ToString() => $"Plan ({Value.Zones.Count} zones, {Value.Provenance.Operation})";
}
public sealed class FloorGoo : PreviewGoo<IFloor>
{
public FloorGoo(IFloor value, Vector3d previewOffset = default)
: base(value, previewOffset)
{
}
public override string TypeName => "Floor";
protected override IEnumerable<Zone> Zones => Value.Zones;
protected override IEnumerable<Surface> Surfaces => Value.Surfaces;
public override IGH_Goo Duplicate() => new FloorGoo(Value, PreviewOffset);
public override string ToString() => $"Floor ({Value.Zones.Count} zones, {Value.Provenance.Operation})";
}
public sealed class BuildingGoo : PreviewGoo<IGeneratedBuilding>
{
private readonly IReadOnlyList<ZonePart> _unmodelled;
/// <param name="value">The building.</param>
/// <param name="previewOffset">Preview Location (D-062).</param>
/// <param name="unmodelled">Parts of the storeys the building represents but does not model, from the aggregator (preview only).</param>
public BuildingGoo(IGeneratedBuilding value, Vector3d previewOffset = default, IReadOnlyList<ZonePart>? unmodelled = null)
: base(value, previewOffset)
{
_unmodelled = unmodelled ?? new ZonePart[0];
}
public override string TypeName => "Building";
protected override IEnumerable<Zone> Zones => Value.Zones;
protected override IEnumerable<Surface> Surfaces => Value.Surfaces;
protected override IEnumerable<ZonePart> UnmodelledParts => _unmodelled;
public override IGH_Goo Duplicate() => new BuildingGoo(Value, PreviewOffset, _unmodelled);
public override string ToString() => $"Building ({Value.Zones.Count} zones, {Value.Provenance.Operation})";
}
- [x] Step 3: Build, check Python, and see the icon script fail
dotnet build BEMGen.sln -c Release
python -c "import sys, PIL; print(sys.version_info[:2] >= (3, 10), PIL.__version__)"
python scripts/make_icons.py
git status --short
Expected: Build succeeded., 0 Warning(s), 0 Error(s) (the icons do not exist yet, and the build embeds whatever PNGs exist); True 12.3.0; the script stops with exit code 1 and the message Classes without an icon: ['ActivityHallPresetComponent', 'CareBedroomPresetComponent', 'CareCommunalPresetComponent', 'CleanCorridorPresetComponent', 'ClinicalSupportPresetComponent', 'CorePresetComponent', 'DiningPresetComponent', 'DirtyCorridorPresetComponent', 'KitchenPresetComponent', 'LobbyPresetComponent', 'MallPresetComponent', 'OfficePresetComponent', 'OperatingTheatrePresetComponent', 'RetailPresetComponent', 'ServicePresetComponent']; icons without a class: [], and writes nothing (git status --short lists only M src/Lod.Grasshopper/Components/PresetComponents.cs and M src/Lod.Grasshopper/Goo/BemGoo.cs). With another Pillow version the script still runs, but the PNG bytes may differ from Step 5's hashes; then Step 6 decides.
- [x] Step 4: Replace
scripts/make_icons.py(full file)
The fifteen motifs are added after stair, and their entries after StairPresetComponent; every other motif and entry is unchanged.
"""Draws the BEMGen Grasshopper icons (D-063).
Every icon is drawn at 96 x 96 pixels on a transparent background and downsampled to 24 x 24 with LANCZOS, then written to
src/Lod.Grasshopper/Icons/<Name>.png, where <Name> is the class name of the component, parameter, or assembly info that shows it.
Each icon is a dark glyph plus the accent colour of its toolbar panel; a parameter uses the panel of the component that makes its object.
Shapes only, no fonts, so the output does not depend on installed fonts.
Run from the repository root: python scripts/make_icons.py
Requires Python 3.10+ and Pillow. Generated with Pillow 12.3.0; the same Pillow version gives the same pixels.
The script stops with an error when an icon has no class in src/Lod.Grasshopper or a class has no icon.
"""
from __future__ import annotations
import re
import sys
from collections.abc import Callable
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / "src" / "Lod.Grasshopper"
OUTPUT = SOURCE / "Icons"
CANVAS = 96
SIZE = 24
LINE = 8 # 2 px after downsampling
DARK = (38, 38, 38, 255)
WHITE = (255, 255, 255, 255)
# One accent per toolbar panel (ComponentCategories).
INFO = (96, 125, 139, 255)
PROGRAM = (230, 140, 30, 255)
GENERATE = (46, 158, 79, 255)
SIMPLIFY = (47, 123, 216, 255)
AGGREGATE = (138, 79, 200, 255)
CONVERT = (208, 69, 58, 255)
INSPECT = (24, 150, 150, 255)
Colour = tuple[int, int, int, int]
Draw = ImageDraw.ImageDraw
Motif = Callable[[Draw, Colour], None]
Painter = Callable[[Image.Image, Colour], None]
def box(d: Draw, xy: tuple[float, float, float, float], fill=None, outline=DARK, width: int = LINE) -> None:
d.rectangle(xy, fill=fill, outline=outline, width=width)
def line(d: Draw, points: list[tuple[float, float]], width: int = LINE, fill=DARK) -> None:
d.line(points, fill=fill, width=width, joint="curve")
def arrow_head(d: Draw, tip: tuple[float, float], direction: str, size: float, fill=DARK) -> None:
x, y = tip
if direction == "right":
d.polygon([(x, y), (x - size, y - size), (x - size, y + size)], fill=fill)
elif direction == "down":
d.polygon([(x, y), (x - size, y - size), (x + size, y - size)], fill=fill)
# --- motifs: each draws inside the 96 x 96 canvas -------------------------------------------------------------
def info(d: Draw, accent) -> None:
d.ellipse((6, 6, 90, 90), fill=accent, outline=DARK, width=LINE)
d.rectangle((42, 40, 54, 74), fill=WHITE)
d.rectangle((42, 20, 54, 32), fill=WHITE)
def assembly(d: Draw, accent) -> None:
# A stepped block: the building at three levels of detail.
box(d, (8, 52, 88, 88), fill=accent)
box(d, (8, 30, 60, 56), fill=accent)
box(d, (8, 8, 36, 34), fill=accent)
def schedule(d: Draw, accent) -> None:
# A 24-hour profile as bars on a time axis.
heights = [16, 48, 72, 36, 60, 76, 24]
for i, h in enumerate(heights):
x = 12 + i * 12
d.rectangle((x, 84 - h, x + 7, 83), fill=accent)
d.rectangle((4, 84, 92, 91), fill=DARK)
d.rectangle((4, 4, 11, 91), fill=DARK)
def load(d: Draw, accent) -> None:
bolt = [(58, 4), (18, 54), (44, 54), (34, 92), (78, 38), (52, 38), (64, 4)]
d.polygon(bolt, fill=accent, outline=DARK, width=6)
def preset_tag(d: Draw, accent) -> None:
tag = [(8, 30), (30, 8), (88, 8), (88, 88), (30, 88), (8, 66)]
d.polygon(tag, fill=accent, outline=DARK, width=LINE)
d.ellipse((24, 40, 40, 56), fill=WHITE, outline=DARK, width=4)
line(d, [(52, 34), (76, 34)], width=6)
line(d, [(52, 48), (76, 48)], width=6)
line(d, [(52, 62), (76, 62)], width=6)
def house(d: Draw, accent) -> None:
d.polygon([(48, 6), (90, 44), (6, 44)], fill=accent, outline=DARK, width=LINE)
box(d, (16, 42, 80, 90), fill=accent)
d.rectangle((40, 60, 56, 90), fill=DARK)
def corridor(d: Draw, accent) -> None:
box(d, (4, 30, 92, 66), fill=accent)
line(d, [(16, 48), (60, 48)], width=8)
arrow_head(d, (78, 48), "right", 12)
def stair(d: Draw, accent) -> None:
steps = [(6, 90), (6, 70), (28, 70), (28, 50), (50, 50), (50, 30), (72, 30), (72, 10), (90, 10), (90, 90)]
d.polygon(steps, fill=accent, outline=DARK, width=LINE)
def office(d: Draw, accent) -> None:
# An office block in elevation with a grid of windows.
box(d, (16, 6, 80, 90), fill=accent)
for y in (18, 38, 58):
for x in (26, 52):
d.rectangle((x, y, x + 16, y + 12), fill=DARK)
d.rectangle((40, 76, 56, 90), fill=DARK)
def core(d: Draw, accent) -> None:
# A service core: a box with two lift doors and an up and down arrow.
box(d, (8, 8, 88, 88), fill=accent)
d.rectangle((20, 34, 42, 80), fill=DARK)
d.rectangle((54, 34, 76, 80), fill=DARK)
d.polygon([(48, 12), (38, 26), (58, 26)], fill=DARK)
def retail(d: Draw, accent) -> None:
# A shopping bag with its handle.
d.arc((30, 6, 66, 46), start=180, end=360, fill=DARK, width=LINE)
d.polygon([(14, 30), (82, 30), (90, 90), (6, 90)], fill=accent, outline=DARK, width=LINE)
def mall(d: Draw, accent) -> None:
# A branching mall in plan: shop blocks (accent) along a T of public routes (white).
box(d, (4, 4, 92, 92), fill=accent)
d.rectangle((40, 8, 56, 88), fill=WHITE)
d.rectangle((8, 40, 40, 56), fill=WHITE)
for a, b in [((40, 4), (40, 40)), ((56, 4), (56, 92)), ((40, 56), (40, 92)), ((4, 40), (40, 40)), ((4, 56), (40, 56))]:
line(d, [a, b], width=4)
def kitchen(d: Draw, accent) -> None:
# A cooking pot with a lid and two handles.
box(d, (16, 40, 80, 86), fill=accent)
line(d, [(4, 50), (16, 50)])
line(d, [(80, 50), (92, 50)])
d.rectangle((12, 28, 84, 38), fill=DARK)
d.rectangle((42, 16, 54, 28), fill=DARK)
def dining(d: Draw, accent) -> None:
# A plate between a fork and a knife.
d.ellipse((26, 22, 74, 70), fill=accent, outline=DARK, width=LINE)
d.ellipse((38, 34, 62, 58), outline=DARK, width=4)
line(d, [(12, 14), (12, 86)], width=6)
line(d, [(84, 14), (84, 86)], width=6)
d.rectangle((80, 14, 88, 46), fill=DARK)
def operating_theatre(d: Draw, accent) -> None:
# A medical cross.
d.polygon(
[(34, 6), (62, 6), (62, 34), (90, 34), (90, 62), (62, 62), (62, 90), (34, 90), (34, 62), (6, 62), (6, 34), (34, 34)],
fill=accent,
outline=DARK,
width=LINE,
)
def clean_corridor(d: Draw, accent) -> None:
# A corridor with a white (clean) route and an arrow.
box(d, (4, 30, 92, 66), fill=accent)
d.rectangle((10, 42, 70, 54), fill=WHITE)
arrow_head(d, (86, 48), "right", 12)
def dirty_corridor(d: Draw, accent) -> None:
# A corridor with a hatched (dirty) route and an arrow.
box(d, (4, 30, 92, 66), fill=accent)
for x in range(12, 66, 14):
line(d, [(x, 60), (x + 10, 36)], width=5)
arrow_head(d, (86, 48), "right", 12)
def clinical_support(d: Draw, accent) -> None:
# Shelves of supplies beside a small medical cross.
box(d, (6, 10, 58, 90), fill=accent)
for y in (34, 58):
line(d, [(6, y), (58, y)], width=6)
d.rectangle((70, 28, 82, 64), fill=DARK)
d.rectangle((58, 40, 94, 52), fill=DARK)
def care_bedroom(d: Draw, accent) -> None:
# A bed in elevation: headboard, mattress, and pillow.
d.rectangle((6, 24, 16, 86), fill=DARK)
box(d, (16, 54, 90, 72), fill=accent)
d.rectangle((20, 42, 42, 54), fill=DARK)
line(d, [(86, 72), (86, 86)])
def care_communal(d: Draw, accent) -> None:
# A round table with four people around it.
d.ellipse((28, 28, 68, 68), fill=accent, outline=DARK, width=LINE)
for x, y in [(48, 12), (84, 48), (48, 84), (12, 48)]:
d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=DARK)
def lobby(d: Draw, accent) -> None:
# A glazed entrance front with an open door and an arrow leading in.
box(d, (6, 10, 90, 88), fill=accent)
line(d, [(34, 10), (34, 88)], width=4)
line(d, [(62, 10), (62, 88)], width=4)
d.rectangle((38, 44, 58, 88), fill=WHITE, outline=DARK, width=4)
arrow_head(d, (48, 84), "down", 8)
def activity_hall(d: Draw, accent) -> None:
# A sports court in plan: the hall, its centre line, and the centre circle.
box(d, (4, 16, 92, 80), fill=accent)
line(d, [(48, 16), (48, 80)], width=5)
d.ellipse((34, 34, 62, 62), outline=DARK, width=5)
def service_rooms(d: Draw, accent) -> None:
# A row of three lockers with their handles.
for left in (6, 36, 66):
box(d, (left, 8, left + 24, 88), fill=accent, width=6)
d.rectangle((left + 15, 40, left + 19, 56), fill=DARK)
def linear_generator(d: Draw, accent) -> None:
# Footprint with a west stair, a corridor band, and dwelling-unit divisions on both sides.
box(d, (4, 12, 91, 83))
d.rectangle((12, 20, 23, 75), fill=DARK)
d.rectangle((28, 40, 87, 55), fill=accent)
for x in (48, 68):
line(d, [(x, 12), (x, 40)])
line(d, [(x, 55), (x, 83)])
def stair_bay_bar(d: Draw, accent) -> None:
# A long bar of two stair bays: dwelling (accent) | stair (dark) | dwelling, repeated, with a party wall between the bays.
box(d, (2, 30, 93, 66), fill=accent)
for x in (20, 66):
d.rectangle((x, 30, x + 10, 66), fill=DARK)
line(d, [(48, 30), (48, 66)], width=4)
def stair_pair(d: Draw, accent) -> None:
# One compact plate: two dwellings (accent) around one full-depth stair (dark).
box(d, (14, 22, 82, 74), fill=accent)
d.rectangle((42, 26, 54, 70), fill=DARK)
def gallery_bar(d: Draw, accent) -> None:
# A bar of dwellings (accent) between two end stairs (dark); the gallery along it is dashed, as it is not modelled.
box(d, (4, 34, 91, 76), fill=accent)
d.rectangle((8, 38, 18, 72), fill=DARK)
d.rectangle((77, 38, 87, 72), fill=DARK)
for x in (36, 54):
line(d, [(x, 34), (x, 76)], width=6)
for x in range(6, 90, 16):
line(d, [(x, 20), (x + 9, 20)], width=6)
def terrace_row(d: Draw, accent) -> None:
# Attached houses in elevation: three gables sharing their side walls.
houses = [(6, 34), (34, 62), (62, 90)]
for left, right in houses:
middle = (left + right) / 2
d.polygon([(left, 46), (middle, 22), (right, 46), (right, 88), (left, 88)], fill=accent, outline=DARK, width=6)
for left, right in houses:
d.rectangle((left + 10, 68, right - 10, 88), fill=DARK)
def point_plate(d: Draw, accent) -> None:
# A square plate: four dwellings (accent) in a pinwheel around one central core (dark) without façades.
box(d, (6, 6, 90, 90), fill=accent)
d.rectangle((36, 36, 60, 60), fill=DARK)
for a, b in [((6, 36), (60, 36)), ((60, 6), (60, 60)), ((36, 60), (90, 60)), ((36, 36), (36, 90))]:
line(d, [a, b], width=6)
def winged_band(d: Draw, accent) -> None:
# A comb in plan: a band (bottom) with three wings projecting north (up), every wing joined to the band along its root; stairs dark.
d.polygon(
[(4, 90), (92, 90), (92, 8), (70, 8), (70, 62), (59, 62), (59, 8), (37, 8), (37, 62), (26, 62), (26, 8), (4, 8)],
fill=accent,
outline=DARK,
width=6,
)
for left, right in ((4, 26), (37, 59), (70, 92)):
d.rectangle((left + 3, 30, right - 3, 38), fill=DARK)
for x in (20, 48, 76):
d.rectangle((x, 66, x + 6, 87), fill=DARK)
def open_court(d: Draw, accent) -> None:
# A U in plan: a north wing (top) across the full width and two side wings, around a court open to the south (bottom); stairs dark.
d.polygon([(4, 92), (28, 92), (28, 34), (68, 34), (68, 92), (92, 92), (92, 6), (4, 6)], fill=accent, outline=DARK, width=6)
for x in (24, 45, 66):
d.rectangle((x, 9, x + 6, 31), fill=DARK)
for left in (4, 68):
d.rectangle((left + 3, 58, left + 21, 66), fill=DARK)
def enclosed_court(d: Draw, accent) -> None:
# A closed ring in plan: south and north wings across the full width and west and east wings between them, around a court (the hole,
# transparent) closed on all four sides; stairs dark.
box(d, (4, 4, 92, 92), fill=accent, width=6)
box(d, (30, 30, 66, 66), fill=(0, 0, 0, 0), width=6)
for y in (7, 69):
for x in (24, 66):
d.rectangle((x, y, x + 6, y + 20), fill=DARK)
for left in (7, 69):
d.rectangle((left, 45, left + 20, 51), fill=DARK)
def transform_plan(d: Draw, accent) -> None:
# A footprint outline and the same footprint rotated and moved, joined by a turning arrow.
box(d, (6, 52, 50, 90), width=6)
d.polygon([(66, 12), (92, 28), (76, 56), (50, 40)], fill=accent, outline=DARK, width=6)
d.arc((18, 20, 70, 72), start=180, end=270, fill=DARK, width=8)
arrow_head(d, (58, 24), "right", 10)
def z0(d: Draw, accent) -> None:
# Many cells: every unit its own zone.
box(d, (4, 4, 91, 91), fill=accent)
for k in (32, 60):
line(d, [(k, 4), (k, 91)])
line(d, [(4, k), (91, k)])
def z1(d: Draw, accent) -> None:
# Cells of one space type merged: two unit rows and a corridor.
box(d, (4, 10, 92, 40), fill=accent)
box(d, (4, 56, 92, 86), fill=accent)
box(d, (4, 36, 92, 60))
def z2(d: Draw, accent) -> None:
# Perimeter ring split on the corner bisectors, plus a core.
box(d, (4, 4, 92, 92))
d.rectangle((30, 30, 66, 66), fill=accent, outline=DARK, width=LINE)
for a, b in [((4, 4), (30, 30)), ((92, 4), (66, 30)), ((4, 92), (30, 66)), ((92, 92), (66, 66))]:
line(d, [a, b], width=6)
def z3(d: Draw, accent) -> None:
box(d, (6, 16, 90, 80), fill=accent)
def slab(d: Draw, accent, y: float, height: float = 16, left: float = 6, right: float = 90) -> None:
box(d, (left, y, right, y + height), fill=accent, width=6)
def stack(d: Draw, accent) -> None:
for y in (8, 36, 64):
slab(d, accent, y, 24)
def multiplier(d: Draw, accent) -> None:
slab(d, accent, 6, 24, 6, 90)
line(d, [(18, 44), (50, 84)], width=10)
line(d, [(50, 44), (18, 84)], width=10)
for y in (44, 58, 72):
d.rectangle((62, y, 90, y + 8), fill=DARK)
def single_zone_per_floor_type(d: Draw, accent) -> None:
# One zone per floor entry: a one-storey zone below a three-storey zone.
box(d, (24, 66, 71, 91), fill=accent)
box(d, (24, 4, 71, 58), fill=accent)
def single_zone_building(d: Draw, accent) -> None:
box(d, (24, 4, 71, 91), fill=accent)
def convert(d: Draw, accent) -> None:
d.polygon([(40, 44), (90, 44), (90, 92), (40, 92)], fill=accent, outline=DARK, width=LINE)
line(d, [(6, 14), (44, 14), (64, 34), (64, 56)], width=10)
arrow_head(d, (64, 74), "down", 16)
def inspect(d: Draw, accent) -> None:
d.ellipse((6, 6, 64, 64), fill=accent, outline=DARK, width=LINE)
d.ellipse((18, 18, 38, 38), fill=WHITE)
line(d, [(56, 56), (88, 88)], width=16)
def mini_grid(d: Draw, origin: tuple[float, float], size: float, cells: int, accent) -> None:
x0, y0 = origin
box(d, (x0, y0, x0 + size, y0 + size), fill=accent, width=6)
step = size / cells
for k in range(1, cells):
line(d, [(x0 + k * step, y0), (x0 + k * step, y0 + size)], width=4)
line(d, [(x0, y0 + k * step), (x0 + size, y0 + k * step)], width=4)
def map_source_target(d: Draw, accent) -> None:
mini_grid(d, (4, 4), 40, 2, accent)
box(d, (52, 52, 92, 92), fill=accent, width=6)
line(d, [(32, 56), (32, 72), (40, 72)], width=8)
arrow_head(d, (52, 72), "right", 12)
line(d, [(56, 26), (72, 26), (72, 36)], width=8)
arrow_head(d, (72, 50), "down", 12)
def validate(d: Draw, accent) -> None:
d.ellipse((4, 4, 92, 92), fill=accent, outline=DARK, width=LINE)
line(d, [(24, 50), (42, 68), (74, 30)], width=14, fill=WHITE)
# Parameter motifs: small versions of their object's motif, drawn inside the hexagon (x and y in 20..76).
def small_schedule(d: Draw, accent) -> None:
for x, h in ((28, 16), (40, 36), (52, 24), (64, 40)):
d.rectangle((x, 72 - h, x + 7, 71), fill=accent)
d.rectangle((24, 72, 75, 75), fill=DARK)
def small_load(d: Draw, accent) -> None:
d.polygon([(54, 20), (30, 52), (46, 52), (40, 76), (68, 44), (52, 44), (60, 20)], fill=accent, outline=DARK, width=4)
def small_preset(d: Draw, accent) -> None:
d.polygon([(20, 36), (32, 24), (76, 24), (76, 72), (32, 72), (20, 60)], fill=accent, outline=DARK, width=4)
d.ellipse((30, 42, 42, 54), fill=WHITE, outline=DARK, width=4)
def small_plan(d: Draw, accent) -> None:
box(d, (20, 24, 75, 71), width=4)
d.rectangle((24, 44, 71, 51), fill=accent)
d.rectangle((24, 28, 31, 67), fill=DARK)
for x in (48,):
line(d, [(x, 24), (x, 44)], width=4)
line(d, [(x, 52), (x, 71)], width=4)
def small_floor(d: Draw, accent) -> None:
box(d, (16, 36, 79, 59), fill=accent, width=4)
def small_building(d: Draw, accent) -> None:
box(d, (32, 16, 63, 79), fill=accent, width=4)
for y in (36, 56):
line(d, [(32, y), (63, y)], width=4)
# --- icons ----------------------------------------------------------------------------------------------------
def component(motif: Motif) -> Painter:
"""A component: its motif at full size."""
def paint(image: Image.Image, accent: Colour) -> None:
motif(ImageDraw.Draw(image), accent)
return paint
def parameter(motif: Motif) -> Painter:
"""A parameter: a small version of its object's motif inside a hexagon (the Grasshopper parameter shape)."""
def paint(image: Image.Image, accent: Colour) -> None:
d = ImageDraw.Draw(image)
d.polygon([(24, 4), (72, 4), (94, 48), (72, 92), (24, 92), (2, 48)], fill=WHITE, outline=DARK, width=4)
motif(d, accent)
return paint
ICONS: dict[str, tuple[Colour, Painter]] = {
"BemGenAssemblyInfo": (INFO, component(assembly)),
"BemGenInfoComponent": (INFO, component(info)),
"ScheduleComponent": (PROGRAM, component(schedule)),
"LoadComponent": (PROGRAM, component(load)),
"ProgramPresetComponent": (PROGRAM, component(preset_tag)),
"DwellingUnitPresetComponent": (PROGRAM, component(house)),
"CorridorPresetComponent": (PROGRAM, component(corridor)),
"StairPresetComponent": (PROGRAM, component(stair)),
"OfficePresetComponent": (PROGRAM, component(office)),
"CorePresetComponent": (PROGRAM, component(core)),
"RetailPresetComponent": (PROGRAM, component(retail)),
"MallPresetComponent": (PROGRAM, component(mall)),
"KitchenPresetComponent": (PROGRAM, component(kitchen)),
"DiningPresetComponent": (PROGRAM, component(dining)),
"OperatingTheatrePresetComponent": (PROGRAM, component(operating_theatre)),
"CleanCorridorPresetComponent": (PROGRAM, component(clean_corridor)),
"DirtyCorridorPresetComponent": (PROGRAM, component(dirty_corridor)),
"ClinicalSupportPresetComponent": (PROGRAM, component(clinical_support)),
"CareBedroomPresetComponent": (PROGRAM, component(care_bedroom)),
"CareCommunalPresetComponent": (PROGRAM, component(care_communal)),
"LobbyPresetComponent": (PROGRAM, component(lobby)),
"ActivityHallPresetComponent": (PROGRAM, component(activity_hall)),
"ServicePresetComponent": (PROGRAM, component(service_rooms)),
"LinearPlanGeneratorComponent": (GENERATE, component(linear_generator)),
"StairBayBarComponent": (GENERATE, component(stair_bay_bar)),
"StairPairComponent": (GENERATE, component(stair_pair)),
"GalleryBarComponent": (GENERATE, component(gallery_bar)),
"TerraceRowComponent": (GENERATE, component(terrace_row)),
"PointPlateComponent": (GENERATE, component(point_plate)),
"WingedBandComponent": (GENERATE, component(winged_band)),
"OpenCourtComponent": (GENERATE, component(open_court)),
"EnclosedCourtComponent": (GENERATE, component(enclosed_court)),
"TransformPlanComponent": (GENERATE, component(transform_plan)),
"NoSimplificationComponent": (SIMPLIFY, component(z0)),
"SemanticMergeComponent": (SIMPLIFY, component(z1)),
"PerimeterCoreComponent": (SIMPLIFY, component(z2)),
"SingleZonePerFloorComponent": (SIMPLIFY, component(z3)),
"StackFloorsComponent": (AGGREGATE, component(stack)),
"StackedFloorZoneMultiplierComponent": (AGGREGATE, component(multiplier)),
"SingleZonePerFloorTypeComponent": (AGGREGATE, component(single_zone_per_floor_type)),
"SingleZoneBuildingComponent": (AGGREGATE, component(single_zone_building)),
"Convert2BemComponent": (CONVERT, component(convert)),
"InspectComponent": (INSPECT, component(inspect)),
"MapSourceToTargetComponent": (INSPECT, component(map_source_target)),
"ValidateComponent": (INSPECT, component(validate)),
"ScheduleParameter": (PROGRAM, parameter(small_schedule)),
"LoadParameter": (PROGRAM, parameter(small_load)),
"ProgramPresetParameter": (PROGRAM, parameter(small_preset)),
"PlanParameter": (GENERATE, parameter(small_plan)),
"FloorParameter": (SIMPLIFY, parameter(small_floor)),
"BuildingParameter": (AGGREGATE, parameter(small_building)),
}
def render(name: str) -> Image.Image:
accent, paint = ICONS[name]
image = Image.new("RGBA", (CANVAS, CANVAS), (0, 0, 0, 0))
paint(image, accent)
return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
def classes_with_icons() -> set[str]:
"""Concrete classes that show an icon: components, parameters, and the assembly info."""
pattern = re.compile(r"public sealed class (\w+) : (\w+)")
names = set()
for path in SOURCE.rglob("*.cs"):
if "obj" in path.parts or "bin" in path.parts:
continue
for name, _ in pattern.findall(path.read_text(encoding="utf-8")):
if name.endswith(("Component", "Parameter")) or name == "BemGenAssemblyInfo":
names.add(name)
return names
def main() -> int:
expected = classes_with_icons()
missing = sorted(expected - ICONS.keys())
unknown = sorted(ICONS.keys() - expected)
if missing or unknown:
print(f"Classes without an icon: {missing}; icons without a class: {unknown}", file=sys.stderr)
return 1
OUTPUT.mkdir(parents=True, exist_ok=True)
stale = sorted(p.name for p in OUTPUT.glob("*.png") if p.stem not in ICONS)
if stale:
print(f"Icons without a class in {OUTPUT}: {stale}", file=sys.stderr)
return 1
for name in sorted(ICONS):
render(name).save(OUTPUT / f"{name}.png", optimize=False)
print(f"Wrote {len(ICONS)} icons to {OUTPUT.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
sys.exit(main())
- [x] Step 5: Draw the icons and check the hashes
python scripts/make_icons.py
git status --short
sha256sum src/Lod.Grasshopper/Icons/*.png
Expected: Wrote 51 icons to src\Lod.Grasshopper\Icons; git status --short shows M scripts/make_icons.py, the two files of Steps 1 and 2, and the fifteen new PNGs (?? src/Lod.Grasshopper/Icons/…PresetComponent.png), and no existing PNG modified; the hashes (Pillow 12.3.0):
a4e0709b6d8cd05a79c33af9ee864918850e74c8aef2c3ac617859fc08ac2fdc *src/Lod.Grasshopper/Icons/ActivityHallPresetComponent.png
606a11f2bbd6ee6cb7edede403166ae0f1b39231fcf5882dff360717df990cae *src/Lod.Grasshopper/Icons/BemGenAssemblyInfo.png
e33ae284b7f017ee11851986e79e4b8449e1b5027ffedf41cad68ed2abbfb15d *src/Lod.Grasshopper/Icons/BemGenInfoComponent.png
431104585e8745167567797eefef526bbf664ba6777101e3677281cb6976bfde *src/Lod.Grasshopper/Icons/BuildingParameter.png
9805fc04d927c05f955ef5dfd0e6913e247bfec90c495a8adf051c36637cf866 *src/Lod.Grasshopper/Icons/CareBedroomPresetComponent.png
88900c3c46841a23766ace6b7a1d22a5a3bbb4e6403c2adc71af1fd3c404ff92 *src/Lod.Grasshopper/Icons/CareCommunalPresetComponent.png
516ecd2a3fbb009d1b541e392965c78a85bdec71b5a6a99b71caf71628cf31f3 *src/Lod.Grasshopper/Icons/CleanCorridorPresetComponent.png
69d26c3e73a28fc34768fdb14ada2e202940d28ec0ef159786d9be9e2ff8ebd0 *src/Lod.Grasshopper/Icons/ClinicalSupportPresetComponent.png
edc0ff9b82e6161bf8bdf3b5acebc8f4a43644d42d845827a101593d91d1a110 *src/Lod.Grasshopper/Icons/Convert2BemComponent.png
b35b3a0f58c4893203982ac209cb6004dd8a3d6264ad58dc0875a43aa6c65342 *src/Lod.Grasshopper/Icons/CorePresetComponent.png
c8a815ff220dafff9f84af05411eab8c43191b4b6f4be2f0b4750385e1d95489 *src/Lod.Grasshopper/Icons/CorridorPresetComponent.png
5e37af921cd4055aeedb4d409d5e966b6515acb880aa28e9080aabf4c593b201 *src/Lod.Grasshopper/Icons/DiningPresetComponent.png
92c7e19aa3faf6fdbf3a4f8d23f6919d18490f19ccf954f41c78efaee741ded6 *src/Lod.Grasshopper/Icons/DirtyCorridorPresetComponent.png
755de6f9d4122d567040d0df98ec3232c76f4f1c0af9da934276fe33585dc528 *src/Lod.Grasshopper/Icons/DwellingUnitPresetComponent.png
04caf35a33a9b6443b7fa808766237aecb235db70ffe0f869002d9d3287223d8 *src/Lod.Grasshopper/Icons/EnclosedCourtComponent.png
324ffc55b8f3aed81902fdaea4ac6293654a194e9f58aa97c9aa088547e584be *src/Lod.Grasshopper/Icons/FloorParameter.png
edc98c6c83c0040d935633c0e8fa014ebeeeeae5ef8f596207fd385ad09a0dbe *src/Lod.Grasshopper/Icons/GalleryBarComponent.png
a7191b8f5e30874eaf7d50f0d4555b3565b77157f83ae584393ab9c3b1c3a792 *src/Lod.Grasshopper/Icons/InspectComponent.png
15d1a772ec51f8411eaa7a61cc847b0344147dbf267badc4b61bdc774359a072 *src/Lod.Grasshopper/Icons/KitchenPresetComponent.png
75b4d98c4ad7cffbec53284781d3ad544ccd8ba762c7fd5c33bfd6d6845e42d4 *src/Lod.Grasshopper/Icons/LinearPlanGeneratorComponent.png
acf5163f017f01a1ebb394861f61750e54f9ae8c730a0a6a614f2120f59feadc *src/Lod.Grasshopper/Icons/LoadComponent.png
5045ceb77bc754553aa717110fab061978ef7b51d0c05e0f7f93788a8158f1d5 *src/Lod.Grasshopper/Icons/LoadParameter.png
197cbe6bd28af5e70cc5987020cf0f91b8bd9641afd81f78596e8be7d820a832 *src/Lod.Grasshopper/Icons/LobbyPresetComponent.png
0f1400bcff9d9791690438d777602fa9e00f8405443d8b5108eefdc1d4c5e667 *src/Lod.Grasshopper/Icons/MallPresetComponent.png
438a45a6b5f3071e4918cb6c0f7e6112e4d0ab9ca0ad1aacdc189b4e5de9ecb8 *src/Lod.Grasshopper/Icons/MapSourceToTargetComponent.png
4a702c6b6ae3c4773678a3594228ce4d1e06c2623bc9eeb5bf1fdaaee8e50c30 *src/Lod.Grasshopper/Icons/NoSimplificationComponent.png
43e15233e02b20e102659703622e13469c5f024f6c587edc9bf2403667390b8f *src/Lod.Grasshopper/Icons/OfficePresetComponent.png
e9504a1cea0d69fe7da893cd10e220358f439b119c3ea86efe6e4d3a9cc29e3e *src/Lod.Grasshopper/Icons/OpenCourtComponent.png
c376a8fbfc51c257f0d57f1174fd46c110356261f3bbd8d895df89e76a977b49 *src/Lod.Grasshopper/Icons/OperatingTheatrePresetComponent.png
ac5d421e2cded0be0f7797510b864cdfcdd019dd46caed4147251923602184b8 *src/Lod.Grasshopper/Icons/PerimeterCoreComponent.png
a4bd3109301f4fccec8fa698ceca5dcfbb22e4dc839d850dd5aa4241b5cca95b *src/Lod.Grasshopper/Icons/PlanParameter.png
cb207d9cb3ccc6df62da1fbfc077921526b91ffb8e329ee225c97163e7014c3b *src/Lod.Grasshopper/Icons/PointPlateComponent.png
13741aac4525cf0c0d991bf3b673e55cd22612324fdc3ef3a8aaaaf0e66dec45 *src/Lod.Grasshopper/Icons/ProgramPresetComponent.png
bd0a92bf623dd4b3f6b9c4279e425adc44a7287751d97e8206a26e3c26e7e400 *src/Lod.Grasshopper/Icons/ProgramPresetParameter.png
0c84c2d19c379dfe78aea82f8ed400848665472bcc5b6159b7f1036d25af3f02 *src/Lod.Grasshopper/Icons/RetailPresetComponent.png
90646f46d781eca01dcdf67554987ee04fdd80c450e5f94647858b5f7e7b613d *src/Lod.Grasshopper/Icons/ScheduleComponent.png
693d5513ffe5f813ed3a77dc7b68cfce31e008b407e6410ac34c9ed374c6bfaa *src/Lod.Grasshopper/Icons/ScheduleParameter.png
8217bd3bbe904a6026ef7434606168b5d466341f356280a623f92278908a4068 *src/Lod.Grasshopper/Icons/SemanticMergeComponent.png
b60466bed4dd2bdcb51bc0488af40bdeb5db9f2c24d70f12d32bf1d1b147b436 *src/Lod.Grasshopper/Icons/ServicePresetComponent.png
682ee28436ffab1bc47382ff9915e27f2c774d0ef22448422d2316d739b5727f *src/Lod.Grasshopper/Icons/SingleZoneBuildingComponent.png
380ce788eef6adc20c2f45723deae73b4b747d84ffe5e2664001e46d21157c49 *src/Lod.Grasshopper/Icons/SingleZonePerFloorComponent.png
b8bb1b2890ce6ac6273f49e863b3a25ae0c05c87dc37e33b5483913e6ac52162 *src/Lod.Grasshopper/Icons/SingleZonePerFloorTypeComponent.png
5859a90fd17bb2072b1971cd31caa0df678f29bf2ad9f279c910dc59c66ca0f9 *src/Lod.Grasshopper/Icons/StackFloorsComponent.png
aa115c4ba9c51350ff291f45c6e16db41be505bc5153b6ef8159165f3286a37f *src/Lod.Grasshopper/Icons/StackedFloorZoneMultiplierComponent.png
9ffba933d6cd515b59259979ce8ce666f1a40ed906af5d5b25875f25458115f9 *src/Lod.Grasshopper/Icons/StairBayBarComponent.png
d25e67dce5eceed6cb2379c55755d38c495b4ee227069ac54bed185ed751910c *src/Lod.Grasshopper/Icons/StairPairComponent.png
939d76b08d77d2d099bd823473f1d86eb8b16983840230cf995f8ade9d0debcc *src/Lod.Grasshopper/Icons/StairPresetComponent.png
ab59722b25ff1a3407cf7344e1c4595142b7a3bed05b7f952d58f92b51ffae34 *src/Lod.Grasshopper/Icons/TerraceRowComponent.png
3ce5e835f0921d1dee8feeaf23a45aecd36c3aedb55be5a8386fdb3e540e5246 *src/Lod.Grasshopper/Icons/TransformPlanComponent.png
20a372402d7ca8ae673db6a0a1299d7a3b6dcae9141d29de2b535cc543d950ce *src/Lod.Grasshopper/Icons/ValidateComponent.png
61af5011fa771797ae48e99ba73cbb7d1cb165a2ec11784187a94dd0fbfbd704 *src/Lod.Grasshopper/Icons/WingedBandComponent.png
Run python scripts/make_icons.py a second time: the hashes do not change.
- [x] Step 6: Look at the icons
Draw a contact sheet of all icons at 8× (nearest neighbour) into the build folder, which git ignores:
python - <<'EOF'
from pathlib import Path
from PIL import Image
icons = sorted(Path("src/Lod.Grasshopper/Icons").glob("*.png"))
cell = 24 * 8 + 16
sheet = Image.new("RGBA", (6 * cell, -(-len(icons) // 6) * cell), (235, 235, 235, 255))
for i, path in enumerate(icons):
big = Image.open(path).convert("RGBA").resize((24 * 8, 24 * 8), Image.Resampling.NEAREST)
sheet.alpha_composite(big, ((i % 6) * cell + 8, (i // 6) * cell + 8))
Path("src/Lod.Grasshopper/bin").mkdir(parents=True, exist_ok=True)
sheet.save("src/Lod.Grasshopper/bin/icon-sheet.png")
print(len(icons), "icons drawn on src/Lod.Grasshopper/bin/icon-sheet.png")
EOF
Expected: 51 icons drawn on src/Lod.Grasshopper/bin/icon-sheet.png.
Open the image. In the order of sorted() on Windows (case-insensitive), six per row: the fifteen new icons are orange like the residential presets. Activity Hall Preset (a sports court with its centre line and circle) is the first icon of the first row, which ends with Care Bedroom Preset (a bed) and Care Communal Preset (a round table with four people); the second row starts with Clean Corridor Preset (a corridor with a white route and an arrow) and Clinical Support Preset (shelves beside a small medical cross) and holds Core Preset (a box with two lift doors and an up and down arrow) fourth and Dining Preset (a plate between a fork and a knife) last; the third row starts with Dirty Corridor Preset (a corridor with a hatched route and an arrow); the fourth row starts with Kitchen Preset (a cooking pot) and ends with Lobby Preset (a glazed entrance front with an open door and an arrow) and Mall Preset (shop blocks along a white T of routes); the fifth row holds Office Preset (an office block with a grid of windows) third and Operating Theatre Preset (a medical cross) fifth; the sixth row holds Retail Preset (a shopping bag) fifth; the seventh row holds Service Preset (three lockers) third. The 36 icons of S8.3 keep their motifs and move to make room; the 51 icons fill eight rows of six and three icons of a ninth.
If the hashes of Step 5 differed (another Pillow version), this check decides: when every icon matches the description above, continue and record the Pillow version in the commit message body; otherwise stop and report the difference. The script is never edited to match hashes.
- [x] Step 7: Build and check the embedded icons
dotnet build BEMGen.sln -c Release
powershell -NoProfile -Command "([Reflection.Assembly]::ReflectionOnlyLoadFrom((Resolve-Path 'src/Lod.Grasshopper/bin/Release/net7.0/BEMGen.gha').Path)).GetManifestResourceNames().Count"
Expected: Build succeeded., 0 Warning(s), 0 Error(s); 51: the 51 resource names Lod.Grasshopper.Icons.<name>.png, one for each PNG of Step 5.
- [x] Step 8: Commit
git add src/Lod.Grasshopper/Components/PresetComponents.cs src/Lod.Grasshopper/Goo/BemGoo.cs scripts/make_icons.py src/Lod.Grasshopper/Icons
git commit -m "feature(grasshopper): add default preset components for the non-residential space types"
Expected before git add: the three modified files and the fifteen new PNGs; nothing under bin/.
- [x] Step 9: Merge Slice B (D-004)
git fetch origin
git switch main
git pull --ff-only
git switch feature/non-residential-programs
git rebase main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
git switch main
git merge --ff-only feature/non-residential-programs
git branch -d feature/non-residential-programs
Expected: VERIFY PASSED, with 193 core, 181 generators, and 603 integration tests passed on net8.0. Do not push (D-098).
Slice C — feature/office-plate-cafeteria¶
Each family is one task: the generator tests first (they fail to build), the generator, the snapshot reviewed and accepted, then the integration matrix, which passes at once because every simplifier and aggregator already handles the new plans; if a case of the matrix fails, stop and report: a pipeline change needs its own decision and test-first task. Task 9 adds the two components and their icons.
Task 7: Office plate around a central service core (SYN-TYP-017)¶
Files:
- Create: src/Lod.Generators/OfficePlate/OfficePlateGenerator.cs
- Test: create tests/Lod.Generators.Tests/OfficePlate/OfficePlateGeneratorTests.cs, tests/Lod.Integration.Tests/OfficePlateTypologyTests.cs
- Snapshot: create tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt
Interfaces:
- Consumes (S2–S8.3): PlanGenerator<TParameters, TPresets> (protected PlanGenerator(ToleranceSettings), Validate, Layout, Describe, protected static bool IsPositive(double)), PlanParameters, PlanPresets.Slots, PresetSlot, PlanLayout(Polygon2 Footprint, IReadOnlyList<PlannedZone> Zones), PlannedZone(ZoneId, string, SpaceType, Polygon2), Polygon2.Rectangle, Polygon2.Create(outer, holes), Provenance.Format, DiagnosticCodes.InvalidParameter, PresetSpaceTypeMismatch; SpaceType.Office and Core, ExampleNonResidentialPresets.Office and Core (Task 5); test support PlanAssertions (TilesTheFootprint, HasOneCentredWindowPerOutdoorWall, Neighbours, OutdoorFacings), Snapshot.Match, TextReport.Describe, Pipeline.Simplifier, Pipeline.Aggregator, Pipeline.Tolerances, PlanTransform.Apply, SemanticMerge, PerimeterCore(ToleranceSettings, double), NoSimplification, SingleZoneBuilding, FloorEntry, BuildingValidator.
- Produces: public sealed record OfficePlateParameters(double PlateLength, double PlateDepth, double CoreLength, double CoreDepth, double FloorHeight, double OrientationDegrees) : PlanParameters with static Default = (42, 30, 18, 6, 3.8, 0); public sealed record OfficePlatePresets(ProgramPreset Office, ProgramPreset Core) : PlanPresets (slots Office, Core); public sealed class OfficePlateGenerator(ToleranceSettings), Name = OfficePlateGenerator, public const double MinimumOfficeDepth = 3.0: with a plate L × D and a core l × d, the core SC (Service Core, Core) is [(L − l)/2, (L + l)/2] × [(D − d)/2, (D + d)/2] and the office OF (Office, Office) is the plate with the core as its one hole; the footprint is the plate, without a hole; errors InvalidParameter for each length that is not positive and finite ({name} must be positive, got {value}.) and, only when those pass, for each axis whose office depth (L − l)/2 or (D − d)/2 is below 3 m (CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core., and the same for CoreDepth at the south and north façades); provenance keys PlateLength, PlateDepth, CoreLength, CoreDepth. By default the core has 108 m² and no outdoor wall, the office 1152 m² and the four façades with one window each (218.88 m² of glazing).
- [x] Step 1: Create the branch
git switch -c feature/office-plate-cafeteria main
- [x] Step 2: Write the failing tests — create
tests/Lod.Generators.Tests/OfficePlate/OfficePlateGeneratorTests.cs(full file)
23 tests: the ADR-006 defaults; the canonical plan (zones SC and OF, names, space types, 1260 m², no footprint hole, 108 m² and 1152 m², the corners of the core, the office with the core as its one hole, 3.8 m storeys, no source zones); tiling for four plate and core sizes; the core without outdoor walls, adjoining only the office over 48 m; the office with every façade; the window rule (4 windows, 218.88 m², no diagnostic); five invalid lengths; a core too close to the west and east façades and to the south and north façades; an office depth of exactly 3 m; every invalid parameter reported at once; preset slots; swapped presets; determinism; provenance; the canonical snapshot.
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Common;
using Lod.Core.Geometry;
using Lod.Core.Model;
using Lod.Core.Programs;
using Lod.Generators.OfficePlate;
using Lod.Tests.Shared;
using Xunit;
namespace Lod.Generators.Tests.OfficePlate;
public sealed class OfficePlateGeneratorTests
{
/// <summary>The example presets, one per slot.</summary>
public static readonly OfficePlatePresets Examples = new(ExampleNonResidentialPresets.Office, ExampleNonResidentialPresets.Core);
private readonly OfficePlateGenerator _generator = new(ToleranceSettings.Default);
[Fact]
public void DefaultsAreTheAdr006Defaults()
{
Assert.Equal(
new OfficePlateParameters(PlateLength: 42.0, PlateDepth: 30.0, CoreLength: 18.0, CoreDepth: 6.0, FloorHeight: 3.8, OrientationDegrees: 0.0),
OfficePlateParameters.Default);
Assert.Equal(3.0, OfficePlateGenerator.MinimumOfficeDepth);
}
[Fact]
public void CanonicalPlateIsOneOfficeRingAroundACentralCore()
{
IGeneratedPlan plan = Generate(OfficePlateParameters.Default);
Assert.Equal(new[] { "SC", "OF" }, plan.Zones.Select(z => z.Id.Value));
Assert.Equal(new[] { "Service Core", "Office" }, plan.Zones.Select(z => z.Name));
Assert.Equal(new[] { SpaceType.Core, SpaceType.Office }, plan.Zones.Select(z => z.SpaceType));
Assert.Equal(42.0 * 30.0, plan.Footprint.Area, 9);
Assert.Empty(plan.Footprint.Holes);
Assert.Equal(new[] { 108.0, 1152.0 }, plan.Zones.Select(z => z.FloorArea));
Polygon2 core = plan.Zones[0].Parts[0].Footprint;
Polygon2 office = plan.Zones[1].Parts[0].Footprint;
Assert.Equal(new[] { new Point2(12.0, 12.0), new Point2(30.0, 12.0), new Point2(30.0, 18.0), new Point2(12.0, 18.0) }, core.Outer);
Assert.Empty(core.Holes);
Assert.Equal(new[] { new Point2(0.0, 0.0), new Point2(42.0, 0.0), new Point2(42.0, 30.0), new Point2(0.0, 30.0) }, office.Outer);
IReadOnlyList<Point2> hole = Assert.Single(office.Holes);
Assert.Equal(core.Outer.OrderBy(p => p.X).ThenBy(p => p.Y), hole.OrderBy(p => p.X).ThenBy(p => p.Y));
Assert.All(plan.Zones, z => Assert.Equal(3.8, z.Parts[0].Height));
Assert.All(plan.Zones, z => Assert.Empty(z.SourceZones));
}
[Theory]
[InlineData(42.0, 30.0, 18.0, 6.0)]
[InlineData(36.0, 36.0, 12.0, 12.0)]
[InlineData(60.0, 20.5, 40.0, 8.5)]
[InlineData(24.5, 18.0, 18.5, 12.0)]
public void ZonesTileThePlate(double plateLength, double plateDepth, double coreLength, double coreDepth)
{
IGeneratedPlan plan = Generate(OfficePlateParameters.Default with { PlateLength = plateLength, PlateDepth = plateDepth, CoreLength = coreLength, CoreDepth = coreDepth });
PlanAssertions.TilesTheFootprint(plan);
Assert.Equal(plateLength * plateDepth, plan.Footprint.Area, 9);
Assert.Equal(coreLength * coreDepth, plan.Zones[0].FloorArea, 9);
Assert.Equal(2, plan.Zones.Count);
}
[Fact]
public void TheCoreHasNoOutdoorWallAndAdjoinsTheOfficeOnEverySide()
{
IGeneratedPlan plan = Generate(OfficePlateParameters.Default);
Assert.Empty(PlanAssertions.OutdoorFacings(plan, "SC"));
Assert.Equal(new[] { "OF" }, PlanAssertions.Neighbours(plan, "SC"));
WallSurface[] coreWalls = plan.Surfaces.OfType<WallSurface>().Where(w => w.Zone.Value == "SC").ToArray();
Assert.Equal(4, coreWalls.Length);
Assert.All(coreWalls, w => Assert.Equal(BoundaryCondition.Interzone, w.Boundary));
Assert.Equal(2.0 * (18.0 + 6.0), coreWalls.Sum(w => w.Length), 9);
Assert.Equal(4, plan.Surfaces.OfType<WallSurface>().Count(w => w.Zone.Value == "OF" && w.AdjacentZone?.Value == "SC"));
}
[Fact]
public void TheOfficeHasEveryFacade()
{
IGeneratedPlan plan = Generate(OfficePlateParameters.Default);
Assert.Equal(new[] { OrientationBin.East, OrientationBin.North, OrientationBin.South, OrientationBin.West }, PlanAssertions.OutdoorFacings(plan, "OF"));
Assert.Equal(2.0 * (42.0 + 30.0), plan.Surfaces.OfType<WallSurface>().Where(w => w.Boundary == BoundaryCondition.Outdoors).Sum(w => w.Length), 9);
}
[Fact]
public void EveryOutdoorWallHasOneCentredWindowSizedByItsPreset()
{
Result<IGeneratedPlan> result = _generator.Generate(OfficePlateParameters.Default, Examples);
Assert.Empty(result.Diagnostics);
PlanAssertions.HasOneCentredWindowPerOutdoorWall(result.Value, Examples);
Assert.Equal(4, result.Value.Surfaces.OfType<WallSurface>().Count(w => w.Windows.Count == 1));
Assert.Equal(0.4 * 2.0 * (42.0 + 30.0) * 3.8, result.Value.Surfaces.OfType<WallSurface>().Sum(w => w.GlazedArea), 9);
}
[Theory]
[InlineData(0.0, 30.0, 18.0, 6.0, "PlateLength")]
[InlineData(42.0, -30.0, 18.0, 6.0, "PlateDepth")]
[InlineData(42.0, 30.0, 0.0, 6.0, "CoreLength")]
[InlineData(42.0, 30.0, 18.0, double.NaN, "CoreDepth")]
[InlineData(double.PositiveInfinity, 30.0, 18.0, 6.0, "PlateLength")]
public void InvalidParametersAreErrors(double plateLength, double plateDepth, double coreLength, double coreDepth, string name)
{
Result<IGeneratedPlan> result = _generator.Generate(new OfficePlateParameters(plateLength, plateDepth, coreLength, coreDepth, 3.8, 0.0), Examples);
Assert.False(result.IsSuccess);
Diagnostic error = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticCodes.InvalidParameter, error.Code);
Assert.StartsWith(name, error.Message);
}
[Fact]
public void ACoreTooCloseToTheWestAndEastFacadesIsAnError()
{
Result<IGeneratedPlan> result = _generator.Generate(OfficePlateParameters.Default with { CoreLength = 40.0 }, Examples);
Diagnostic error = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticCodes.InvalidParameter, error.Code);
Assert.Equal(
"CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.",
error.Message);
}
[Fact]
public void ACoreTooCloseToTheSouthAndNorthFacadesIsAnError()
{
Result<IGeneratedPlan> result = _generator.Generate(OfficePlateParameters.Default with { CoreDepth = 26.0 }, Examples);
Diagnostic error = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticCodes.InvalidParameter, error.Code);
Assert.Equal(
"CoreDepth 26 m leaves an office depth of 2 m at the south and north façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.",
error.Message);
}
[Fact]
public void AnOfficeDepthOfExactlyTheMinimumIsValid()
{
Result<IGeneratedPlan> result = _generator.Generate(OfficePlateParameters.Default with { CoreLength = 36.0, CoreDepth = 24.0 }, Examples);
Assert.True(result.IsSuccess, string.Join("; ", result.Diagnostics));
Assert.Equal((42.0 * 30.0) - (36.0 * 24.0), result.Value.Zones[1].FloorArea, 9);
}
[Fact]
public void EveryInvalidParameterIsReported()
{
Result<IGeneratedPlan> result = _generator.Generate(new OfficePlateParameters(0.0, -30.0, 0.0, 6.0, 0.0, double.NaN), Examples);
Assert.Equal(5, result.Diagnostics.Count(d => d.Code == DiagnosticCodes.InvalidParameter));
}
[Fact]
public void PresetsHaveOneSlotPerSpaceTypeInOrder()
{
Assert.Equal(new[] { ("Office", SpaceType.Office), ("Core", SpaceType.Core) }, Examples.Slots.Select(s => (s.Input, s.SpaceType)));
Assert.Equal(new[] { Examples.Office, Examples.Core }, Examples.Slots.Select(s => s.Preset));
}
[Fact]
public void SwappedPresetsAreAnErrorPerWrongSlot()
{
Result<IGeneratedPlan> result = _generator.Generate(
OfficePlateParameters.Default,
new OfficePlatePresets(ExampleNonResidentialPresets.Core, ExampleNonResidentialPresets.Office));
Assert.Equal(new[] { "Office", "Core" }, result.Diagnostics.Where(d => d.Code == DiagnosticCodes.PresetSpaceTypeMismatch).Select(d => d.Subject));
}
[Fact]
public void GenerationIsDeterministic()
{
Assert.Equal(TextReport.Describe(Generate(OfficePlateParameters.Default)), TextReport.Describe(Generate(OfficePlateParameters.Default)));
}
[Fact]
public void ProvenanceRecordsParametersAndPresets()
{
Provenance provenance = Generate(OfficePlateParameters.Default).Provenance;
Assert.Equal(nameof(OfficePlateGenerator), provenance.Operation);
Assert.Equal(
new[]
{
"CoreDepth=6", "CoreLength=18", "FloorHeight=3.8", "OrientationDegrees=0", "PlateDepth=30", "PlateLength=42",
"Presets=Office:Example Office;Core:Example Core",
},
provenance.Parameters.Select(p => $"{p.Key}={p.Value}"));
}
[Fact]
public void CanonicalPlanSnapshot()
{
Snapshot.Match(TextReport.Describe(Generate(OfficePlateParameters.Default)), "office-plate-canonical");
}
private IGeneratedPlan Generate(OfficePlateParameters parameters) => _generator.Generate(parameters, Examples).Value;
}
- [x] Step 3: Run the tests and see them fail
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.OfficePlate.OfficePlateGeneratorTests"
Expected: the build fails because the namespace and its types do not exist yet, with these errors for the net8.0 target (in this order):
tests\Lod.Generators.Tests\OfficePlate\OfficePlateGeneratorTests.cs(7,22): error CS0234: The type or namespace name 'OfficePlate' does not exist in the namespace 'Lod.Generators' (are you missing an assembly reference?)
tests\Lod.Generators.Tests\OfficePlate\OfficePlateGeneratorTests.cs(201,37): error CS0246: The type or namespace name 'OfficePlateParameters' could not be found (are you missing a using directive or an assembly reference?)
tests\Lod.Generators.Tests\OfficePlate\OfficePlateGeneratorTests.cs(16,28): error CS0246: The type or namespace name 'OfficePlatePresets' could not be found (are you missing a using directive or an assembly reference?)
tests\Lod.Generators.Tests\OfficePlate\OfficePlateGeneratorTests.cs(18,22): error CS0246: The type or namespace name 'OfficePlateGenerator' could not be found (are you missing a using directive or an assembly reference?)
- [x] Step 4: Implement — create
src/Lod.Generators/OfficePlate/OfficePlateGenerator.cs(full file)
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Common;
using Lod.Core.Geometry;
using Lod.Core.Model;
using Lod.Core.Plans;
using Lod.Core.Programs;
namespace Lod.Generators.OfficePlate;
/// <summary>Parameters of the office plate around a central service core (SYN-TYP-017, ADR-006).</summary>
/// <param name="PlateLength">Plate length along plan x, m.</param>
/// <param name="PlateDepth">Plate depth along plan y, m.</param>
/// <param name="CoreLength">Length of the central core along plan x, m.</param>
/// <param name="CoreDepth">Depth of the central core along plan y, m.</param>
/// <param name="FloorHeight">Floor-to-floor height, m.</param>
/// <param name="OrientationDegrees">Clockwise rotation of plan north from true north, degrees.</param>
public sealed record OfficePlateParameters(
double PlateLength,
double PlateDepth,
double CoreLength,
double CoreDepth,
double FloorHeight,
double OrientationDegrees)
: PlanParameters(FloorHeight, OrientationDegrees)
{
/// <summary>The ADR-006 defaults: a 42 m × 30 m plate around an 18 m × 6 m core (12 m of office on every side), 3.8 m storeys, orientation 0°.</summary>
public static OfficePlateParameters Default { get; } = new(42.0, 30.0, 18.0, 6.0, 3.8, 0.0);
}
/// <summary>The program presets of the office plate, one per space type it places (D-061).</summary>
/// <param name="Office">Preset of the office work area; space type <see cref="SpaceType.Office"/>.</param>
/// <param name="Core">Preset of the service core; space type <see cref="SpaceType.Core"/>.</param>
public sealed record OfficePlatePresets(ProgramPreset Office, ProgramPreset Core) : PlanPresets
{
/// <inheritdoc />
public override IReadOnlyList<PresetSlot> Slots => new[]
{
new PresetSlot(nameof(Office), SpaceType.Office, Office),
new PresetSlot(nameof(Core), SpaceType.Core, Core),
};
}
/// <summary>
/// Central service core within an office plate (SYN-TYP-017, ADR-006): a rectangular plate whose office work area surrounds a central core on all
/// four sides (R001) and whose core holds the lifts and shared services (R002), a <see cref="SpaceType.Core"/> zone without an outdoor wall. The
/// work area is one department zone (D-097, ADR-014): the plate minus the core, a polygon with the core as its hole; the footprint has no hole.
/// </summary>
/// <remarks>
/// Zone IDs: <c>SC</c> (the core), then <c>OF</c> (the office). With a plate <c>L × D</c> and a core <c>l × d</c>, the core is
/// <c>[(L − l)/2, (L + l)/2] × [(D − d)/2, (D + d)/2]</c>. The core must leave at least <see cref="MinimumOfficeDepth"/> of work area on every side
/// (R003/R004: a core near a façade is outside the family).
/// </remarks>
public sealed class OfficePlateGenerator : PlanGenerator<OfficePlateParameters, OfficePlatePresets>
{
/// <summary>The smallest office depth between the core and a façade, m: a row of desks with its aisle (ADR-006).</summary>
public const double MinimumOfficeDepth = 3.0;
/// <summary>Creates the generator.</summary>
/// <param name="tolerances">Tolerances.</param>
public OfficePlateGenerator(ToleranceSettings tolerances)
: base(tolerances)
{
}
/// <inheritdoc />
public override string Name => nameof(OfficePlateGenerator);
/// <inheritdoc />
protected override IEnumerable<Diagnostic> Validate(OfficePlateParameters parameters)
{
var values = new (string Name, double Value)[]
{
(nameof(OfficePlateParameters.PlateLength), parameters.PlateLength),
(nameof(OfficePlateParameters.PlateDepth), parameters.PlateDepth),
(nameof(OfficePlateParameters.CoreLength), parameters.CoreLength),
(nameof(OfficePlateParameters.CoreDepth), parameters.CoreDepth),
};
var errors = values
.Where(v => !IsPositive(v.Value))
.Select(v => Diagnostic.Error(DiagnosticCodes.InvalidParameter, $"{v.Name} must be positive, got {v.Value}."))
.ToList();
if (errors.Count == 0)
{
errors.AddRange(DepthError(nameof(OfficePlateParameters.CoreLength), parameters.CoreLength, parameters.PlateLength, "west and east"));
errors.AddRange(DepthError(nameof(OfficePlateParameters.CoreDepth), parameters.CoreDepth, parameters.PlateDepth, "south and north"));
}
return errors;
}
/// <inheritdoc />
protected override PlanLayout Layout(OfficePlateParameters p)
{
double west = (p.PlateLength - p.CoreLength) / 2.0;
double south = (p.PlateDepth - p.CoreDepth) / 2.0;
Polygon2 core = Polygon2.Rectangle(west, south, west + p.CoreLength, south + p.CoreDepth);
Polygon2 plate = Polygon2.Rectangle(0.0, 0.0, p.PlateLength, p.PlateDepth);
Polygon2 office = Polygon2.Create(plate.Outer, new[] { core.Outer }).Value;
var zones = new[]
{
new PlannedZone(new ZoneId("SC"), "Service Core", SpaceType.Core, core),
new PlannedZone(new ZoneId("OF"), "Office", SpaceType.Office, office),
};
return new PlanLayout(plate, zones);
}
/// <inheritdoc />
protected override IEnumerable<KeyValuePair<string, string>> Describe(OfficePlateParameters p) => new[]
{
new KeyValuePair<string, string>(nameof(p.PlateLength), Provenance.Format(p.PlateLength)),
new KeyValuePair<string, string>(nameof(p.PlateDepth), Provenance.Format(p.PlateDepth)),
new KeyValuePair<string, string>(nameof(p.CoreLength), Provenance.Format(p.CoreLength)),
new KeyValuePair<string, string>(nameof(p.CoreDepth), Provenance.Format(p.CoreDepth)),
};
private static IEnumerable<Diagnostic> DepthError(string name, double core, double plate, string facades)
{
double depth = (plate - core) / 2.0;
if (depth >= MinimumOfficeDepth)
{
yield break;
}
yield return Diagnostic.Error(
DiagnosticCodes.InvalidParameter,
$"{name} {Provenance.Format(core)} m leaves an office depth of {Provenance.Format(depth)} m at the {facades} façades; "
+ $"SYN-TYP-017 needs at least {Provenance.Format(MinimumOfficeDepth)} m of work area on every side of the core.");
}
}
- [x] Step 5: Run the tests and see the snapshot fail
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.OfficePlate.OfficePlateGeneratorTests"
Expected on net8.0:
Failed! - Failed: 1, Passed: 22, Skipped: 0, Total: 23, Duration: … - Lod.Generators.Tests.dll (net8.0)
with the only failing test and its message:
Failed Lod.Generators.Tests.OfficePlate.OfficePlateGeneratorTests.CanonicalPlanSnapshot
Snapshot 'office-plate-canonical' is missing or differs. Review Snapshots/office-plate-canonical.received.txt and rename it to office-plate-canonical.txt to accept.
and the file tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.received.txt (git-ignored by *.received.*).
- [x] Step 6: Review and accept the received file
Review the received file against the content below; it must be identical: 28 lines, 2 zones, 16 surfaces (4 outdoor walls, each with one window; 8 interzone walls; 4 floors and ceilings), glazing 63.84 m² south, 63.84 m² north, 45.6 m² east, 45.6 m² west (218.88 m² in all); the core's four walls are interzone walls to the office, and the office's walls OF/W5–OF/W8 face into the core, so its floor and ceiling have the ring's 1152 m². Accept it:
Move-Item -Force tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.received.txt tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt
tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt:
footprint area=1260.000000 floorHeight=3.800000 orientation=0.000000
zone SC Core "Service Core" area=108.000000 volume=410.400000 parts=1 multiplier=1 sources=[]
load Lighting PerFloorArea value=5.000000 annual=2695.650000
load ElectricEquipment PerFloorArea value=3.000000 annual=3526.800000
load Infiltration AirChangesPerHour value=0.300000 annual=8760.000000
setpoints heatingMean=21.000000 coolingMean=24.000000
zone OF Office "Office" area=1152.000000 volume=4377.600000 parts=1 multiplier=1 sources=[]
load Occupancy PerFloorArea value=0.100000 annual=2231.550000
load Lighting PerFloorArea value=10.000000 annual=2695.650000
load ElectricEquipment PerFloorArea value=10.000000 annual=3526.800000
load Infiltration AirChangesPerHour value=0.300000 annual=8760.000000
setpoints heatingMean=21.000000 coolingMean=24.000000
surface SC/W1 Interzone adjacent=OF area=68.400000 wall (12.000000,12.000000)-(30.000000,12.000000) z=0.000000 facing=South glazing=0.000000
surface SC/W2 Interzone adjacent=OF area=22.800000 wall (30.000000,12.000000)-(30.000000,18.000000) z=0.000000 facing=East glazing=0.000000
surface SC/W3 Interzone adjacent=OF area=68.400000 wall (30.000000,18.000000)-(12.000000,18.000000) z=0.000000 facing=North glazing=0.000000
surface SC/W4 Interzone adjacent=OF area=22.800000 wall (12.000000,18.000000)-(12.000000,12.000000) z=0.000000 facing=West glazing=0.000000
surface SC/F Unresolved area=108.000000 Floor z=0.000000
surface SC/C Unresolved area=108.000000 Ceiling z=3.800000
surface OF/W1 Outdoors area=159.600000 wall (0.000000,0.000000)-(42.000000,0.000000) z=0.000000 facing=South glazing=63.840000 window(offset=7.718434 sill=0.698334 26.563132x2.403331)
surface OF/W2 Outdoors area=114.000000 wall (42.000000,0.000000)-(42.000000,30.000000) z=0.000000 facing=East glazing=45.600000 window(offset=5.513167 sill=0.698334 18.973666x2.403331)
surface OF/W3 Outdoors area=159.600000 wall (42.000000,30.000000)-(0.000000,30.000000) z=0.000000 facing=North glazing=63.840000 window(offset=7.718434 sill=0.698334 26.563132x2.403331)
surface OF/W4 Outdoors area=114.000000 wall (0.000000,30.000000)-(0.000000,0.000000) z=0.000000 facing=West glazing=45.600000 window(offset=5.513167 sill=0.698334 18.973666x2.403331)
surface OF/W5 Interzone adjacent=SC area=68.400000 wall (12.000000,18.000000)-(30.000000,18.000000) z=0.000000 facing=South glazing=0.000000
surface OF/W6 Interzone adjacent=SC area=22.800000 wall (30.000000,18.000000)-(30.000000,12.000000) z=0.000000 facing=West glazing=0.000000
surface OF/W7 Interzone adjacent=SC area=68.400000 wall (30.000000,12.000000)-(12.000000,12.000000) z=0.000000 facing=North glazing=0.000000
surface OF/W8 Interzone adjacent=SC area=22.800000 wall (12.000000,12.000000)-(12.000000,18.000000) z=0.000000 facing=East glazing=0.000000
surface OF/F Unresolved area=1152.000000 Floor z=0.000000
surface OF/C Unresolved area=1152.000000 Ceiling z=3.800000
- [x] Step 7: Run the tests and see them pass
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.OfficePlate.OfficePlateGeneratorTests"
Expected: Passed! - Failed: 0, Passed: 23, Skipped: 0, Total: 23, Duration: … - Lod.Generators.Tests.dll (net8.0).
- [x] Step 8: Write the integration tests — create
tests/Lod.Integration.Tests/OfficePlateTypologyTests.cs(full file)
51 cases: EveryCombinationValidates (16), EveryCombinationValidatesOnARotatedAndTranslatedPlan (32, rotated by 30° and moved by (5.3, −2.1) m, and rotated by 131.4° and moved by (15.08, −153.9) m), SemanticMergeKeepsTheOfficeRingAroundTheCore (the office keeps its hole and 1152 m², the core no outdoor wall), PerimeterCoreSplitsTheOfficeRingAndKeepsTheServiceCoreInItsCore (P-North, P-East, P-South, P-West, CORE; CORE without a hole, of (42 − 9.14) × (30 − 9.14) m², sourced from OF and SC; every perimeter zone sourced from OF only), and SingleZoneBuildingDropsTheRingAndKeepsEveryFacadeWindow (one zone, no part with a hole, 20 outdoor walls with one window each). No production code changes.
using System;
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Buildings;
using Lod.Core.Common;
using Lod.Core.Geometry;
using Lod.Core.Model;
using Lod.Core.Plans;
using Lod.Core.Programs;
using Lod.Core.Simplification;
using Lod.Core.Validation;
using Lod.Generators.OfficePlate;
using Xunit;
namespace Lod.Integration.Tests;
/// <summary>
/// The S8.4 office plate (SYN-TYP-017, ADR-006): its work area is a ring around the core, the first zone with a hole in a footprint without one,
/// through every simplifier and aggregator, canonical and placed.
/// </summary>
public sealed class OfficePlateTypologyTests
{
private static readonly string[] Simplifiers = { "NoSimplification", "SemanticMerge", "PerimeterCore", "SingleZonePerFloor" };
private static readonly string[] Aggregators = { "Stack", "StackedFloorZoneMultiplier", "SingleZonePerFloorType", "SingleZoneBuilding" };
/// <summary>Rotations (degrees) and translations (m) applied by <see cref="PlanTransform"/>.</summary>
private static readonly double[][] Placements = { new[] { 30.0, 5.3, -2.1 }, new[] { 131.4, 15.08, -153.9 } };
public static IEnumerable<object[]> Matrix() =>
from simplifier in Simplifiers
from aggregator in Aggregators
select new object[] { simplifier, aggregator };
public static IEnumerable<object[]> PlacedMatrix() =>
from combination in Matrix()
from placement in Enumerable.Range(0, Placements.Length)
select combination.Append(placement).ToArray();
[Theory]
[MemberData(nameof(Matrix))]
public void EveryCombinationValidates(string simplifier, string aggregator)
{
AssertValidates(Plan(), simplifier, aggregator);
}
[Theory]
[MemberData(nameof(PlacedMatrix))]
public void EveryCombinationValidatesOnARotatedAndTranslatedPlan(string simplifier, string aggregator, int placement)
{
double[] p = Placements[placement];
AssertValidates(PlanTransform.Apply(Plan(), p[0], p[1], p[2]).Value, simplifier, aggregator);
}
[Fact]
public void SemanticMergeKeepsTheOfficeRingAroundTheCore()
{
IFloor floor = new SemanticMerge(Pipeline.Tolerances).Simplify(Plan()).Value;
Assert.Equal(new[] { SpaceType.Core, SpaceType.Office }, floor.Zones.Select(z => z.SpaceType).OrderBy(t => t));
Zone office = floor.Zones.Single(z => z.SpaceType == SpaceType.Office);
Assert.Single(office.Parts[0].Footprint.Holes);
Assert.Equal(1152.0, office.FloorArea, 6);
Assert.DoesNotContain(floor.Surfaces.OfType<WallSurface>(), w => w.Zone == floor.Zones.Single(z => z.SpaceType == SpaceType.Core).Id && w.Boundary == BoundaryCondition.Outdoors);
}
[Fact]
public void PerimeterCoreSplitsTheOfficeRingAndKeepsTheServiceCoreInItsCore()
{
IFloor floor = new PerimeterCore(Pipeline.Tolerances, 4.57).Simplify(Plan()).Value;
Assert.Equal("P-North,P-East,P-South,P-West,CORE", string.Join(",", floor.Zones.Select(z => z.Id.Value)));
Zone core = floor.Zones.Single(z => z.Id.Value == "CORE");
Assert.Empty(core.Parts[0].Footprint.Holes);
Assert.Equal((42.0 - (2 * 4.57)) * (30.0 - (2 * 4.57)), core.FloorArea, 6);
Assert.Equal(new[] { "OF", "SC" }, core.SourceZones.Select(z => z.Value).OrderBy(id => id, StringComparer.Ordinal));
Assert.All(floor.Zones.Where(z => z.Id.Value.StartsWith("P-", StringComparison.Ordinal)), z => Assert.Equal(new[] { "OF" }, z.SourceZones.Select(s => s.Value)));
}
[Fact]
public void SingleZoneBuildingDropsTheRingAndKeepsEveryFacadeWindow()
{
IFloor floor = new NoSimplification().Simplify(Plan()).Value;
IGeneratedBuilding building = new SingleZoneBuilding(Pipeline.Tolerances).Aggregate(new[] { new FloorEntry(floor, 1), new FloorEntry(floor, 3), new FloorEntry(floor, 1) }).Value;
Zone zone = Assert.Single(building.Zones);
Assert.All(zone.Parts, p => Assert.Empty(p.Footprint.Holes));
Assert.Equal(5 * 4, building.Surfaces.OfType<WallSurface>().Count(w => w.Boundary == BoundaryCondition.Outdoors && w.Windows.Count == 1));
}
private static void AssertValidates(IGeneratedPlan plan, string simplifier, string aggregator)
{
Result<IFloor> floor = Pipeline.Simplifier(simplifier).Simplify(plan);
Assert.True(floor.IsSuccess, string.Join("; ", floor.Diagnostics));
FloorEntry[] entries = { new(floor.Value, 1), new(floor.Value, 3), new(floor.Value, 1) };
Result<IGeneratedBuilding> building = Pipeline.Aggregator(aggregator).Aggregate(entries);
Assert.True(building.IsSuccess, string.Join("; ", building.Diagnostics));
ValidationReport report = new BuildingValidator(Pipeline.Tolerances).Validate(building.Value);
Assert.True(report.Passed, report.Describe());
Assert.DoesNotContain(building.Diagnostics, d => d.Severity == DiagnosticSeverity.Warning);
}
private static OfficePlatePresets Presets() => new(ExampleNonResidentialPresets.Office, ExampleNonResidentialPresets.Core);
/// <summary>The canonical plan: ADR-006 defaults and the example presets.</summary>
private static IGeneratedPlan Plan() => new OfficePlateGenerator(Pipeline.Tolerances).Generate(OfficePlateParameters.Default, Presets()).Value;
}
- [x] Step 9: Run the tests, build, and test
dotnet test tests/Lod.Integration.Tests -c Release --filter "FullyQualifiedName~Lod.Integration.Tests.OfficePlateTypologyTests"
dotnet build BEMGen.sln -c Release
dotnet test BEMGen.sln -c Release
git status --short --untracked-files=all
Expected: Passed! - Failed: 0, Passed: 51, Skipped: 0, Total: 51, Duration: … - Lod.Integration.Tests.dll (net8.0); Build succeeded., 0 Warning(s), 0 Error(s); 193 core, 204 generators, and 654 integration tests passed on net8.0; git status lists the four new files of this task and no *.received.txt. The surface builder, every simplifier, and every aggregator accept a zone with a hole in a footprint without one, so no Lod.Core change is needed.
- [x] Step 10: Commit
git add src/Lod.Generators/OfficePlate/OfficePlateGenerator.cs tests/Lod.Generators.Tests/OfficePlate/OfficePlateGeneratorTests.cs tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt tests/Lod.Integration.Tests/OfficePlateTypologyTests.cs
git commit -m "feature(generators): add the office plate around a central service core"
Task 8: Cafeteria service edge beside a dining field (SYN-TYP-016)¶
Files:
- Create: src/Lod.Generators/Cafeteria/CafeteriaGenerator.cs
- Test: create tests/Lod.Generators.Tests/Cafeteria/CafeteriaGeneratorTests.cs, tests/Lod.Integration.Tests/CafeteriaTypologyTests.cs
- Snapshot: create tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt
Interfaces:
- Consumes: as Task 7, with SpaceType.Kitchen and Dining and ExampleNonResidentialPresets.Kitchen and Dining (Task 5).
- Produces: public sealed record CafeteriaParameters(double Length, double DiningDepth, double KitchenDepth, double FloorHeight, double OrientationDegrees) : PlanParameters with static Default = (30, 15, 8, 4, 0); public sealed record CafeteriaPresets(ProgramPreset Kitchen, ProgramPreset Dining) : PlanPresets (slots Kitchen, Dining); public sealed class CafeteriaGenerator(ToleranceSettings), Name = CafeteriaGenerator: with length L, dining depth Dd, and kitchen depth Dk, KS (Kitchen and Servery, Kitchen) is [0, L] × [Dd, Dd + Dk] and DI (Dining, Dining) is [0, L] × [0, Dd], so the dining field gets the south side; errors InvalidParameter for each length that is not positive and finite and, only when those pass, for a kitchen not shallower than the dining field (KitchenDepth 15 m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.); provenance keys Length, DiningDepth, KitchenDepth. By default 240 m² and 450 m², one 30 m interzone wall, and 6 windows (114.4 m² of glazing).
- [x] Step 1: Write the failing tests — create
tests/Lod.Generators.Tests/Cafeteria/CafeteriaGeneratorTests.cs(full file)
20 tests: the ADR-006 defaults; the canonical plan (zones KS and DI, their rectangles and areas); tiling for three sizes; the service edge sharing the dining field's whole north edge; the window rule (6 windows, no diagnostic); five invalid lengths; a kitchen as deep as or deeper than the dining field; every invalid parameter reported at once; preset slots; swapped presets; determinism; provenance; the canonical snapshot.
using System.Linq;
using Lod.Core.Common;
using Lod.Core.Geometry;
using Lod.Core.Model;
using Lod.Core.Programs;
using Lod.Generators.Cafeteria;
using Lod.Tests.Shared;
using Xunit;
namespace Lod.Generators.Tests.Cafeteria;
public sealed class CafeteriaGeneratorTests
{
/// <summary>The example presets, one per slot.</summary>
public static readonly CafeteriaPresets Examples = new(ExampleNonResidentialPresets.Kitchen, ExampleNonResidentialPresets.Dining);
private readonly CafeteriaGenerator _generator = new(ToleranceSettings.Default);
[Fact]
public void DefaultsAreTheAdr006Defaults()
{
Assert.Equal(new CafeteriaParameters(Length: 30.0, DiningDepth: 15.0, KitchenDepth: 8.0, FloorHeight: 4.0, OrientationDegrees: 0.0), CafeteriaParameters.Default);
}
[Fact]
public void CanonicalPlanIsAServiceEdgeAlongTheNorthOfTheDiningField()
{
IGeneratedPlan plan = Generate(CafeteriaParameters.Default);
Assert.Equal(new[] { "KS", "DI" }, plan.Zones.Select(z => z.Id.Value));
Assert.Equal(new[] { "Kitchen and Servery", "Dining" }, plan.Zones.Select(z => z.Name));
Assert.Equal(new[] { SpaceType.Kitchen, SpaceType.Dining }, plan.Zones.Select(z => z.SpaceType));
Assert.Equal(30.0 * 23.0, plan.Footprint.Area, 9);
Assert.Equal(new[] { 240.0, 450.0 }, plan.Zones.Select(z => z.FloorArea));
Assert.Equal(new[] { new Point2(0.0, 15.0), new Point2(30.0, 15.0), new Point2(30.0, 23.0), new Point2(0.0, 23.0) }, plan.Zones[0].Parts[0].Footprint.Outer);
Assert.Equal(new[] { new Point2(0.0, 0.0), new Point2(30.0, 0.0), new Point2(30.0, 15.0), new Point2(0.0, 15.0) }, plan.Zones[1].Parts[0].Footprint.Outer);
Assert.All(plan.Zones, z => Assert.Equal(4.0, z.Parts[0].Height));
Assert.All(plan.Zones, z => Assert.Empty(z.SourceZones));
}
[Theory]
[InlineData(30.0, 15.0, 8.0)]
[InlineData(12.5, 20.0, 4.5)]
[InlineData(48.0, 9.0, 8.9)]
public void ZonesTileThePlan(double length, double diningDepth, double kitchenDepth)
{
IGeneratedPlan plan = Generate(CafeteriaParameters.Default with { Length = length, DiningDepth = diningDepth, KitchenDepth = kitchenDepth });
PlanAssertions.TilesTheFootprint(plan);
Assert.Equal(length * (diningDepth + kitchenDepth), plan.Footprint.Area, 9);
Assert.Equal(2, plan.Zones.Count);
}
[Fact]
public void TheServiceEdgeSharesTheDiningFieldsWholeNorthEdge()
{
IGeneratedPlan plan = Generate(CafeteriaParameters.Default);
WallSurface edge = Assert.Single(plan.Surfaces.OfType<WallSurface>(), w => w.Zone.Value == "KS" && w.Boundary == BoundaryCondition.Interzone);
Assert.Equal("DI", edge.AdjacentZone!.Value.Value);
Assert.Equal(30.0, edge.Length, 9);
Assert.Equal(new[] { OrientationBin.East, OrientationBin.North, OrientationBin.West }, PlanAssertions.OutdoorFacings(plan, "KS"));
Assert.Equal(new[] { OrientationBin.East, OrientationBin.South, OrientationBin.West }, PlanAssertions.OutdoorFacings(plan, "DI"));
}
[Fact]
public void EveryOutdoorWallHasOneCentredWindowSizedByItsPreset()
{
Result<IGeneratedPlan> result = _generator.Generate(CafeteriaParameters.Default, Examples);
Assert.Empty(result.Diagnostics);
PlanAssertions.HasOneCentredWindowPerOutdoorWall(result.Value, Examples);
Assert.Equal(6, result.Value.Surfaces.OfType<WallSurface>().Count(w => w.Windows.Count == 1));
}
[Theory]
[InlineData(0.0, 15.0, 8.0, "Length")]
[InlineData(30.0, -15.0, 8.0, "DiningDepth")]
[InlineData(30.0, 15.0, 0.0, "KitchenDepth")]
[InlineData(30.0, 15.0, double.NaN, "KitchenDepth")]
[InlineData(double.PositiveInfinity, 15.0, 8.0, "Length")]
public void InvalidParametersAreErrors(double length, double diningDepth, double kitchenDepth, string name)
{
Result<IGeneratedPlan> result = _generator.Generate(new CafeteriaParameters(length, diningDepth, kitchenDepth, 4.0, 0.0), Examples);
Assert.False(result.IsSuccess);
Diagnostic error = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticCodes.InvalidParameter, error.Code);
Assert.StartsWith(name, error.Message);
}
[Theory]
[InlineData(15.0)]
[InlineData(20.0)]
public void AKitchenNotSmallerThanTheDiningFieldIsAnError(double kitchenDepth)
{
Result<IGeneratedPlan> result = _generator.Generate(CafeteriaParameters.Default with { KitchenDepth = kitchenDepth }, Examples);
Diagnostic error = Assert.Single(result.Diagnostics);
Assert.Equal(DiagnosticCodes.InvalidParameter, error.Code);
Assert.Equal(
$"KitchenDepth {kitchenDepth} m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.",
error.Message);
}
[Fact]
public void EveryInvalidParameterIsReported()
{
Result<IGeneratedPlan> result = _generator.Generate(new CafeteriaParameters(0.0, -15.0, 0.0, 0.0, double.NaN), Examples);
Assert.Equal(5, result.Diagnostics.Count(d => d.Code == DiagnosticCodes.InvalidParameter));
}
[Fact]
public void PresetsHaveOneSlotPerSpaceTypeInOrder()
{
Assert.Equal(new[] { ("Kitchen", SpaceType.Kitchen), ("Dining", SpaceType.Dining) }, Examples.Slots.Select(s => (s.Input, s.SpaceType)));
Assert.Equal(new[] { Examples.Kitchen, Examples.Dining }, Examples.Slots.Select(s => s.Preset));
}
[Fact]
public void SwappedPresetsAreAnErrorPerWrongSlot()
{
Result<IGeneratedPlan> result = _generator.Generate(
CafeteriaParameters.Default,
new CafeteriaPresets(ExampleNonResidentialPresets.Dining, ExampleNonResidentialPresets.Kitchen));
Assert.Equal(new[] { "Kitchen", "Dining" }, result.Diagnostics.Where(d => d.Code == DiagnosticCodes.PresetSpaceTypeMismatch).Select(d => d.Subject));
}
[Fact]
public void GenerationIsDeterministic()
{
Assert.Equal(TextReport.Describe(Generate(CafeteriaParameters.Default)), TextReport.Describe(Generate(CafeteriaParameters.Default)));
}
[Fact]
public void ProvenanceRecordsParametersAndPresets()
{
Provenance provenance = Generate(CafeteriaParameters.Default).Provenance;
Assert.Equal(nameof(CafeteriaGenerator), provenance.Operation);
Assert.Equal(
new[] { "DiningDepth=15", "FloorHeight=4", "KitchenDepth=8", "Length=30", "OrientationDegrees=0", "Presets=Kitchen:Example Kitchen;Dining:Example Dining" },
provenance.Parameters.Select(p => $"{p.Key}={p.Value}"));
}
[Fact]
public void CanonicalPlanSnapshot()
{
Snapshot.Match(TextReport.Describe(Generate(CafeteriaParameters.Default)), "cafeteria-canonical");
}
private IGeneratedPlan Generate(CafeteriaParameters parameters) => _generator.Generate(parameters, Examples).Value;
}
- [x] Step 2: Run the tests and see them fail
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.Cafeteria.CafeteriaGeneratorTests"
Expected: the build fails because the namespace and its types do not exist yet, with these errors for the net8.0 target (in this order):
tests\Lod.Generators.Tests\Cafeteria\CafeteriaGeneratorTests.cs(6,22): error CS0234: The type or namespace name 'Cafeteria' does not exist in the namespace 'Lod.Generators' (are you missing an assembly reference?)
tests\Lod.Generators.Tests\Cafeteria\CafeteriaGeneratorTests.cs(154,37): error CS0246: The type or namespace name 'CafeteriaParameters' could not be found (are you missing a using directive or an assembly reference?)
tests\Lod.Generators.Tests\Cafeteria\CafeteriaGeneratorTests.cs(15,28): error CS0246: The type or namespace name 'CafeteriaPresets' could not be found (are you missing a using directive or an assembly reference?)
tests\Lod.Generators.Tests\Cafeteria\CafeteriaGeneratorTests.cs(17,22): error CS0246: The type or namespace name 'CafeteriaGenerator' could not be found (are you missing a using directive or an assembly reference?)
- [x] Step 3: Implement — create
src/Lod.Generators/Cafeteria/CafeteriaGenerator.cs(full file)
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Common;
using Lod.Core.Geometry;
using Lod.Core.Model;
using Lod.Core.Plans;
using Lod.Core.Programs;
namespace Lod.Generators.Cafeteria;
/// <summary>Parameters of the cafeteria service edge beside a dining field (SYN-TYP-016, ADR-006).</summary>
/// <param name="Length">Length of the service edge and the dining field along plan x, m.</param>
/// <param name="DiningDepth">Depth of the dining field along plan y, m.</param>
/// <param name="KitchenDepth">Depth of the kitchen and servery along plan y, m; smaller than <paramref name="DiningDepth"/>.</param>
/// <param name="FloorHeight">Floor-to-floor height, m.</param>
/// <param name="OrientationDegrees">Clockwise rotation of plan north from true north, degrees.</param>
public sealed record CafeteriaParameters(
double Length,
double DiningDepth,
double KitchenDepth,
double FloorHeight,
double OrientationDegrees)
: PlanParameters(FloorHeight, OrientationDegrees)
{
/// <summary>The ADR-006 defaults: a 30 m service edge, a 15 m deep dining field, an 8 m deep kitchen, 4 m storeys, orientation 0°.</summary>
public static CafeteriaParameters Default { get; } = new(30.0, 15.0, 8.0, 4.0, 0.0);
}
/// <summary>The program presets of the cafeteria, one per space type it places (D-061).</summary>
/// <param name="Kitchen">Preset of the kitchen and servery; space type <see cref="SpaceType.Kitchen"/>.</param>
/// <param name="Dining">Preset of the dining field; space type <see cref="SpaceType.Dining"/>.</param>
public sealed record CafeteriaPresets(ProgramPreset Kitchen, ProgramPreset Dining) : PlanPresets
{
/// <inheritdoc />
public override IReadOnlyList<PresetSlot> Slots => new[]
{
new PresetSlot(nameof(Kitchen), SpaceType.Kitchen, Kitchen),
new PresetSlot(nameof(Dining), SpaceType.Dining, Dining),
};
}
/// <summary>
/// Cafeteria service edge beside a dining field (SYN-TYP-016, ADR-006): food preparation and servery, one department zone (D-097, ADR-014), lie along
/// the whole north edge of the dining field (R001); the servery counter is the kitchen's front on that shared edge, so customers move past it within
/// the dining field into the dining space (R002), and the route is not a zone of its own. The dining field is the larger part (R003/R004).
/// </summary>
/// <remarks>
/// Zone IDs: <c>KS</c> (kitchen and servery, <c>[0, L] × [Dd, Dd + Dk]</c>), then <c>DI</c> (dining, <c>[0, L] × [0, Dd]</c>), for length
/// <c>L</c>, dining depth <c>Dd</c>, and kitchen depth <c>Dk</c>.
/// </remarks>
public sealed class CafeteriaGenerator : PlanGenerator<CafeteriaParameters, CafeteriaPresets>
{
/// <summary>Creates the generator.</summary>
/// <param name="tolerances">Tolerances.</param>
public CafeteriaGenerator(ToleranceSettings tolerances)
: base(tolerances)
{
}
/// <inheritdoc />
public override string Name => nameof(CafeteriaGenerator);
/// <inheritdoc />
protected override IEnumerable<Diagnostic> Validate(CafeteriaParameters parameters)
{
var values = new (string Name, double Value)[]
{
(nameof(CafeteriaParameters.Length), parameters.Length),
(nameof(CafeteriaParameters.DiningDepth), parameters.DiningDepth),
(nameof(CafeteriaParameters.KitchenDepth), parameters.KitchenDepth),
};
var errors = values
.Where(v => !IsPositive(v.Value))
.Select(v => Diagnostic.Error(DiagnosticCodes.InvalidParameter, $"{v.Name} must be positive, got {v.Value}."))
.ToList();
if (errors.Count == 0 && parameters.KitchenDepth >= parameters.DiningDepth)
{
errors.Add(Diagnostic.Error(
DiagnosticCodes.InvalidParameter,
$"{nameof(CafeteriaParameters.KitchenDepth)} {Provenance.Format(parameters.KitchenDepth)} m must be smaller than "
+ $"{nameof(CafeteriaParameters.DiningDepth)} {Provenance.Format(parameters.DiningDepth)} m: in SYN-TYP-016 the dining field is the larger part of the plan."));
}
return errors;
}
/// <inheritdoc />
protected override PlanLayout Layout(CafeteriaParameters p)
{
double north = p.DiningDepth + p.KitchenDepth;
var zones = new[]
{
new PlannedZone(new ZoneId("KS"), "Kitchen and Servery", SpaceType.Kitchen, Polygon2.Rectangle(0.0, p.DiningDepth, p.Length, north)),
new PlannedZone(new ZoneId("DI"), "Dining", SpaceType.Dining, Polygon2.Rectangle(0.0, 0.0, p.Length, p.DiningDepth)),
};
return new PlanLayout(Polygon2.Rectangle(0.0, 0.0, p.Length, north), zones);
}
/// <inheritdoc />
protected override IEnumerable<KeyValuePair<string, string>> Describe(CafeteriaParameters p) => new[]
{
new KeyValuePair<string, string>(nameof(p.Length), Provenance.Format(p.Length)),
new KeyValuePair<string, string>(nameof(p.DiningDepth), Provenance.Format(p.DiningDepth)),
new KeyValuePair<string, string>(nameof(p.KitchenDepth), Provenance.Format(p.KitchenDepth)),
};
}
- [x] Step 4: Run the tests and see the snapshot fail
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.Cafeteria.CafeteriaGeneratorTests"
Expected on net8.0:
Failed! - Failed: 1, Passed: 19, Skipped: 0, Total: 20, Duration: … - Lod.Generators.Tests.dll (net8.0)
with the only failing test and its message:
Failed Lod.Generators.Tests.Cafeteria.CafeteriaGeneratorTests.CanonicalPlanSnapshot
Snapshot 'cafeteria-canonical' is missing or differs. Review Snapshots/cafeteria-canonical.received.txt and rename it to cafeteria-canonical.txt to accept.
and the file tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.received.txt (git-ignored).
- [x] Step 5: Review and accept the received file
Review the received file against the content below; it must be identical: 27 lines, 2 zones, 12 surfaces (6 outdoor walls, each with one window; 2 interzone walls; 4 floors and ceilings), glazing 48 m² south, 12 m² north, 27.2 m² east, 27.2 m² west (114.4 m² in all). Accept it:
Move-Item -Force tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.received.txt tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt
tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt:
footprint area=690.000000 floorHeight=4.000000 orientation=0.000000
zone KS Kitchen "Kitchen and Servery" area=240.000000 volume=960.000000 parts=1 multiplier=1 sources=[]
load Occupancy PerFloorArea value=0.050000 annual=2244.600000
load Lighting PerFloorArea value=12.000000 annual=2610.000000
load ElectricEquipment PerFloorArea value=30.000000 annual=2952.600000
load GasEquipment PerFloorArea value=40.000000 annual=1122.300000
load Ventilation AirChangesPerHour value=15.000000 annual=2610.000000
load Infiltration AirChangesPerHour value=0.300000 annual=8760.000000
setpoints heatingMean=18.000000 coolingMean=26.000000
zone DI Dining "Dining" area=450.000000 volume=1800.000000 parts=1 multiplier=1 sources=[]
load Occupancy PerFloorArea value=0.600000 annual=809.100000
load Lighting PerFloorArea value=10.000000 annual=2349.000000
load ElectricEquipment PerFloorArea value=2.000000 annual=2349.000000
load Infiltration AirChangesPerHour value=0.300000 annual=8760.000000
setpoints heatingMean=21.000000 coolingMean=24.000000
surface KS/W1 Interzone adjacent=DI area=120.000000 wall (0.000000,15.000000)-(30.000000,15.000000) z=0.000000 facing=South glazing=0.000000
surface KS/W2 Outdoors area=32.000000 wall (30.000000,15.000000)-(30.000000,23.000000) z=0.000000 facing=East glazing=3.200000 window(offset=2.735089 sill=1.367544 2.529822x1.264911)
surface KS/W3 Outdoors area=120.000000 wall (30.000000,23.000000)-(0.000000,23.000000) z=0.000000 facing=North glazing=12.000000 window(offset=10.256584 sill=1.367544 9.486833x1.264911)
surface KS/W4 Outdoors area=32.000000 wall (0.000000,23.000000)-(0.000000,15.000000) z=0.000000 facing=West glazing=3.200000 window(offset=2.735089 sill=1.367544 2.529822x1.264911)
surface KS/F Unresolved area=240.000000 Floor z=0.000000
surface KS/C Unresolved area=240.000000 Ceiling z=4.000000
surface DI/W1 Outdoors area=120.000000 wall (0.000000,0.000000)-(30.000000,0.000000) z=0.000000 facing=South glazing=48.000000 window(offset=5.513167 sill=0.735089 18.973666x2.529822)
surface DI/W2 Outdoors area=60.000000 wall (30.000000,0.000000)-(30.000000,15.000000) z=0.000000 facing=East glazing=24.000000 window(offset=2.756584 sill=0.735089 9.486833x2.529822)
surface DI/W3 Interzone adjacent=KS area=120.000000 wall (30.000000,15.000000)-(0.000000,15.000000) z=0.000000 facing=North glazing=0.000000
surface DI/W4 Outdoors area=60.000000 wall (0.000000,15.000000)-(0.000000,0.000000) z=0.000000 facing=West glazing=24.000000 window(offset=2.756584 sill=0.735089 9.486833x2.529822)
surface DI/F Unresolved area=450.000000 Floor z=0.000000
surface DI/C Unresolved area=450.000000 Ceiling z=4.000000
- [x] Step 6: Run the tests and see them pass
dotnet test tests/Lod.Generators.Tests -c Release --filter "FullyQualifiedName~Lod.Generators.Tests.Cafeteria.CafeteriaGeneratorTests"
Expected: Passed! - Failed: 0, Passed: 20, Skipped: 0, Total: 20, Duration: … - Lod.Generators.Tests.dll (net8.0).
- [x] Step 7: Write the integration tests — create
tests/Lod.Integration.Tests/CafeteriaTypologyTests.cs(full file)
50 cases: EveryCombinationValidates (16), EveryCombinationValidatesOnARotatedAndTranslatedPlan (32), SemanticMergeKeepsTheKitchenAndTheDiningFieldApart (240 m² and 450 m²), and PerimeterCoreMixesTheKitchenAndTheDiningFieldInTheNorthAndSidePerimeters (P-North from KS only, P-South from DI only, P-East from both, every zone Mixed). No production code changes.
using System;
using System.Collections.Generic;
using System.Linq;
using Lod.Core.Buildings;
using Lod.Core.Common;
using Lod.Core.Model;
using Lod.Core.Plans;
using Lod.Core.Programs;
using Lod.Core.Simplification;
using Lod.Core.Validation;
using Lod.Generators.Cafeteria;
using Xunit;
namespace Lod.Integration.Tests;
/// <summary>The S8.4 cafeteria (SYN-TYP-016, ADR-006) through every simplifier and aggregator, canonical and placed.</summary>
public sealed class CafeteriaTypologyTests
{
private static readonly string[] Simplifiers = { "NoSimplification", "SemanticMerge", "PerimeterCore", "SingleZonePerFloor" };
private static readonly string[] Aggregators = { "Stack", "StackedFloorZoneMultiplier", "SingleZonePerFloorType", "SingleZoneBuilding" };
/// <summary>Rotations (degrees) and translations (m) applied by <see cref="PlanTransform"/>.</summary>
private static readonly double[][] Placements = { new[] { 30.0, 5.3, -2.1 }, new[] { 131.4, 15.08, -153.9 } };
public static IEnumerable<object[]> Matrix() =>
from simplifier in Simplifiers
from aggregator in Aggregators
select new object[] { simplifier, aggregator };
public static IEnumerable<object[]> PlacedMatrix() =>
from combination in Matrix()
from placement in Enumerable.Range(0, Placements.Length)
select combination.Append(placement).ToArray();
[Theory]
[MemberData(nameof(Matrix))]
public void EveryCombinationValidates(string simplifier, string aggregator)
{
AssertValidates(Plan(), simplifier, aggregator);
}
[Theory]
[MemberData(nameof(PlacedMatrix))]
public void EveryCombinationValidatesOnARotatedAndTranslatedPlan(string simplifier, string aggregator, int placement)
{
double[] p = Placements[placement];
AssertValidates(PlanTransform.Apply(Plan(), p[0], p[1], p[2]).Value, simplifier, aggregator);
}
[Fact]
public void SemanticMergeKeepsTheKitchenAndTheDiningFieldApart()
{
IFloor floor = new SemanticMerge(Pipeline.Tolerances).Simplify(Plan()).Value;
Assert.Equal(new[] { SpaceType.Kitchen, SpaceType.Dining }, floor.Zones.Select(z => z.SpaceType).OrderBy(t => t));
Assert.Equal(new[] { 240.0, 450.0 }, floor.Zones.OrderBy(z => z.SpaceType).Select(z => Math.Round(z.FloorArea, 6)));
}
[Fact]
public void PerimeterCoreMixesTheKitchenAndTheDiningFieldInTheNorthAndSidePerimeters()
{
IFloor floor = new PerimeterCore(Pipeline.Tolerances, 4.57).Simplify(Plan()).Value;
Assert.Equal("P-North,P-East,P-South,P-West,CORE", string.Join(",", floor.Zones.Select(z => z.Id.Value)));
Assert.Equal(new[] { "KS" }, floor.Zones.Single(z => z.Id.Value == "P-North").SourceZones.Select(z => z.Value));
Assert.Equal(new[] { "DI" }, floor.Zones.Single(z => z.Id.Value == "P-South").SourceZones.Select(z => z.Value));
Assert.Equal(new[] { "DI", "KS" }, floor.Zones.Single(z => z.Id.Value == "P-East").SourceZones.Select(z => z.Value).OrderBy(id => id, StringComparer.Ordinal));
Assert.All(floor.Zones, z => Assert.Equal(SpaceType.Mixed, z.SpaceType));
}
private static void AssertValidates(IGeneratedPlan plan, string simplifier, string aggregator)
{
Result<IFloor> floor = Pipeline.Simplifier(simplifier).Simplify(plan);
Assert.True(floor.IsSuccess, string.Join("; ", floor.Diagnostics));
FloorEntry[] entries = { new(floor.Value, 1), new(floor.Value, 3), new(floor.Value, 1) };
Result<IGeneratedBuilding> building = Pipeline.Aggregator(aggregator).Aggregate(entries);
Assert.True(building.IsSuccess, string.Join("; ", building.Diagnostics));
ValidationReport report = new BuildingValidator(Pipeline.Tolerances).Validate(building.Value);
Assert.True(report.Passed, report.Describe());
Assert.DoesNotContain(building.Diagnostics, d => d.Severity == DiagnosticSeverity.Warning);
}
private static CafeteriaPresets Presets() => new(ExampleNonResidentialPresets.Kitchen, ExampleNonResidentialPresets.Dining);
/// <summary>The canonical plan: ADR-006 defaults and the example presets.</summary>
private static IGeneratedPlan Plan() => new CafeteriaGenerator(Pipeline.Tolerances).Generate(CafeteriaParameters.Default, Presets()).Value;
}
- [x] Step 8: Run the tests, build, and test
dotnet test tests/Lod.Integration.Tests -c Release --filter "FullyQualifiedName~Lod.Integration.Tests.CafeteriaTypologyTests"
dotnet build BEMGen.sln -c Release
dotnet test BEMGen.sln -c Release
git status --short --untracked-files=all
Expected: Passed! - Failed: 0, Passed: 50, Skipped: 0, Total: 50, Duration: … - Lod.Integration.Tests.dll (net8.0); Build succeeded., 0 Warning(s), 0 Error(s); 193 core, 224 generators, and 704 integration tests passed on net8.0; git status lists the four new files of this task and no *.received.txt.
- [x] Step 9: Commit
git add src/Lod.Generators/Cafeteria/CafeteriaGenerator.cs tests/Lod.Generators.Tests/Cafeteria/CafeteriaGeneratorTests.cs tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt tests/Lod.Integration.Tests/CafeteriaTypologyTests.cs
git commit -m "feature(generators): add the cafeteria service edge beside a dining field"
Task 9: Office Plate and Cafeteria components, with icons¶
Files:
- Modify: src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs (S8.1 file, last changed in S8.3), scripts/make_icons.py (Task 6)
- Create (generated): src/Lod.Grasshopper/Icons/OfficePlateComponent.png, src/Lod.Grasshopper/Icons/CafeteriaComponent.png
Interfaces:
- Consumes: OfficePlateGenerator, CafeteriaGenerator, and their records (Tasks 7 and 8); the S8.1 base TypologyGeneratorComponent (panel 2 Generate, one Plan output, AddPreset, TryNumbers, Preset, Output, FloorHeightDescription, OrientationDescription); ComponentSupport.PreviewLocationParameter() (S4.1); IconLibrary.For.
- Produces: two components after Enclosed Court in the file, with their number inputs defaulting to the ADR-006 defaults, Floor Height, Orientation, one required preset input per space type (an unconnected one is Grasshopper's missing-input warning, D-065), and Preview Location last (D-062); L/D rather than PL, which is the nickname of Preview Location:
| Component (nickname) | Inputs → output | Class | GUID |
|---|---|---|---|
| Office Plate (OPlate) | Plate Length (L, 42), Plate Depth (D, 30), Core Length (CL, 18), Core Depth (CD, 6), Floor Height (FH, 3.8), Orientation (O, 0), Office (Of), Core (Co), Preview Location (PL) → Plan | OfficePlateComponent |
783cefaa-b836-4dbc-b16d-8e74820ed361 |
| Cafeteria (Cafe) | Length (L, 30), Dining Depth (DD, 15), Kitchen Depth (KD, 8), Floor Height (FH, 4), Orientation (O, 0), Kitchen (Ki), Dining (Di), Preview Location (PL) → Plan | CafeteriaComponent |
95956724-ddbc-48ce-9874-0ec6c244c8fe |
The motifs office_plate (a plate around a dark core) and cafeteria (a dark kitchen band above a dining field with tables), in the Generate green, mapped after EnclosedCourtComponent. 46 components, 6 parameters, and 53 icons. The eight earlier typology components and their base class are unchanged.
- [x] Step 1: Replace
src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs(full file)
Two usings and the two component classes at the end are added; nothing else changes.
using System;
using System.Drawing;
using Grasshopper.Kernel;
using Lod.Core.Common;
using Lod.Core.Model;
using Lod.Core.Programs;
using Lod.Generators.Cafeteria;
using Lod.Generators.EnclosedCourt;
using Lod.Generators.GalleryBar;
using Lod.Generators.OfficePlate;
using Lod.Generators.OpenCourt;
using Lod.Generators.PointPlate;
using Lod.Generators.StairBayBar;
using Lod.Generators.StairPair;
using Lod.Generators.TerraceRow;
using Lod.Generators.WingedBand;
using Lod.Grasshopper.Goo;
using Lod.Grasshopper.Parameters;
namespace Lod.Grasshopper.Components;
/// <summary>
/// Shared inputs and output of the typology plan generator components (ADR-006): number inputs with the family's defaults, one required preset input
/// per space type (D-061, D-065), the Preview Location last (D-062), and one Plan output.
/// </summary>
public abstract class TypologyGeneratorComponent : GH_Component
{
protected const string FloorHeightDescription = "Floor-to-floor height, m.";
protected const string OrientationDescription = "Clockwise rotation of plan north from true north, degrees.";
protected const string Metres = " All lengths in metres regardless of model units.";
protected TypologyGeneratorComponent(string name, string nickname, string description)
: base(name, nickname, description + Metres, ComponentCategories.Category, ComponentCategories.Generate)
{
}
protected override Bitmap? Icon => IconLibrary.For(this);
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddParameter(new PlanParameter(), "Plan", "Plan", "The detailed plan.", GH_ParamAccess.item);
}
/// <summary>Adds the required preset input of one space type.</summary>
protected static void AddPreset(GH_InputParamManager pManager, string name, string nickname, SpaceType spaceType, string role) =>
pManager.AddParameter(new ProgramPresetParameter(), name, nickname, $"Program preset of {role}; space type {spaceType}.", GH_ParamAccess.item);
/// <summary>Reads <paramref name="count"/> number inputs from <paramref name="first"/>; <c>false</c> when one is missing.</summary>
protected static bool TryNumbers(IGH_DataAccess DA, int first, int count, out double[] values)
{
values = new double[count];
for (int i = 0; i < count; i++)
{
if (!DA.GetData(first + i, ref values[i]))
{
return false;
}
}
return true;
}
/// <summary>Reads a preset input; <c>null</c> when it is unconnected, which Grasshopper reports as a missing input (D-065).</summary>
protected static ProgramPreset? Preset(IGH_DataAccess DA, int index)
{
ProgramPresetGoo? goo = null;
return DA.GetData(index, ref goo) && goo is not null ? goo.Value : null;
}
/// <summary>Reports the generator's diagnostics and outputs the plan with this component's preview location.</summary>
protected void Output(IGH_DataAccess DA, Result<IGeneratedPlan> result)
{
IGeneratedPlan? plan = ComponentSupport.Unwrap(this, result);
if (plan is not null)
{
DA.SetData(0, new PlanGoo(plan, ComponentSupport.PreviewLocation(this, DA)));
}
}
}
public sealed class StairBayBarComponent : TypologyGeneratorComponent
{
public StairBayBarComponent()
: base(
"Stair Bay Bar",
"SBar",
"Repeated stair bays in a linear bar (HDHM-TYP-001, ADR-006): bays of dwelling | stair | dwelling through the full depth, each stair serving the two dwellings beside it; no corridor. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("911c3204-42bb-4023-bdb7-2931ea1caad3");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
StairBayBarParameters d = StairBayBarParameters.Default;
pManager.AddIntegerParameter("Bay Count", "BC", "Number of stair bays along the bar; at least 2.", GH_ParamAccess.item, d.BayCount);
pManager.AddNumberParameter("Unit Width", "UW", "Width of each dwelling along the bar, m.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Depth", "D", "Bar depth, which every dwelling and stair spans, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of each stair along the bar, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the stairs");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
int bays = 0;
if (!DA.GetData(0, ref bays) || !TryNumbers(DA, 1, 5, out double[] v) || Preset(DA, 6) is not { } dwellingUnit || Preset(DA, 7) is not { } stair)
{
return;
}
var parameters = new StairBayBarParameters(bays, v[0], v[1], v[2], v[3], v[4]);
Output(DA, new StairBayBarGenerator(ToleranceSettings.Default).Generate(parameters, new StairBayBarPresets(dwellingUnit, stair)));
}
}
public sealed class StairPairComponent : TypologyGeneratorComponent
{
public StairPairComponent()
: base(
"Stair Pair",
"SPair",
"One stair between a dwelling pair (HDHM-TYP-008, ADR-006): one stair bay of the Stair Bay Bar as its own building, the stair serving both dwellings. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("5669d39b-45e6-47ea-9250-f2304d005335");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
StairPairParameters d = StairPairParameters.Default;
pManager.AddNumberParameter("Unit Width", "UW", "Width of each dwelling, m.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Depth", "D", "Plate depth, which both dwellings and the stair span, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of the stair between the dwellings, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the two dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the stair");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 5, out double[] v) || Preset(DA, 5) is not { } dwellingUnit || Preset(DA, 6) is not { } stair)
{
return;
}
var parameters = new StairPairParameters(v[0], v[1], v[2], v[3], v[4]);
Output(DA, new StairPairGenerator(ToleranceSettings.Default).Generate(parameters, new StairPairPresets(dwellingUnit, stair)));
}
}
public sealed class GalleryBarComponent : TypologyGeneratorComponent
{
public GalleryBarComponent()
: base(
"Gallery Bar",
"GBar",
"Continuous external gallery beside a linear bar (HDHM-TYP-002, ADR-006): a row of dwellings through the bar depth with a stair at each end. The gallery is not modelled (D-083): no gallery zone or geometry; its façade is an ordinary outdoor façade. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("a58df462-b3c2-46b7-be3d-7ea9f4629280");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
GalleryBarParameters d = GalleryBarParameters.Default;
pManager.AddIntegerParameter("Unit Count", "UC", "Number of dwellings along the bar; at least 2.", GH_ParamAccess.item, d.UnitCount);
pManager.AddNumberParameter("Unit Width", "UW", "Width of each dwelling along the bar, m.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Depth", "D", "Bar depth, which every dwelling and both stairs span, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of each end stair along the bar, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the two end stairs");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
int units = 0;
if (!DA.GetData(0, ref units) || !TryNumbers(DA, 1, 5, out double[] v) || Preset(DA, 6) is not { } dwellingUnit || Preset(DA, 7) is not { } stair)
{
return;
}
var parameters = new GalleryBarParameters(units, v[0], v[1], v[2], v[3], v[4]);
Output(DA, new GalleryBarGenerator(ToleranceSettings.Default).Generate(parameters, new GalleryBarPresets(dwellingUnit, stair)));
}
}
public sealed class TerraceRowComponent : TypologyGeneratorComponent
{
public TerraceRowComponent()
: base(
"Terrace Row",
"Terr",
"Attached narrow terrace row (HDHM-TYP-006, ADR-006): a row of attached house plots sharing dividing walls, one dwelling zone per plot and storey; each house's own stair is inside the dwelling and not zoned.")
{
}
public override Guid ComponentGuid => new("f694e595-dba2-4b98-9d1e-f1492e3edf39");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
TerraceRowParameters d = TerraceRowParameters.Default;
pManager.AddIntegerParameter("Plot Count", "PC", "Number of attached house plots along the row; at least 2.", GH_ParamAccess.item, d.PlotCount);
pManager.AddNumberParameter("Plot Width", "PW", "Width of each plot along the row, m.", GH_ParamAccess.item, d.PlotWidth);
pManager.AddNumberParameter("Depth", "D", "Row depth, which every house spans, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the houses");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
int plots = 0;
if (!DA.GetData(0, ref plots) || !TryNumbers(DA, 1, 4, out double[] v) || Preset(DA, 5) is not { } dwellingUnit)
{
return;
}
var parameters = new TerraceRowParameters(plots, v[0], v[1], v[2], v[3]);
Output(DA, new TerraceRowGenerator(ToleranceSettings.Default).Generate(parameters, new TerraceRowPresets(dwellingUnit)));
}
}
public sealed class PointPlateComponent : TypologyGeneratorComponent
{
public PointPlateComponent()
: base(
"Point Plate",
"PPlate",
"Compact point-access tower plate (HDHM-TYP-007, ADR-006): a square plate with one central core, a Stair zone without outdoor walls, and four dwellings in a pinwheel around it, each adjoining the core and with two façades. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("8ac9044a-8fbe-4964-a9de-2ef8be35094a");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
PointPlateParameters d = PointPlateParameters.Default;
pManager.AddNumberParameter("Dwelling Depth", "DD", "Depth of each dwelling from its façade to the core, m; the plate side is Core Width + 2 × Dwelling Depth.", GH_ParamAccess.item, d.DwellingDepth);
pManager.AddNumberParameter("Core Width", "CW", "Side of the square core, m.", GH_ParamAccess.item, d.CoreWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the four dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the core");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 4, out double[] v) || Preset(DA, 4) is not { } dwellingUnit || Preset(DA, 5) is not { } stair)
{
return;
}
var parameters = new PointPlateParameters(v[0], v[1], v[2], v[3]);
Output(DA, new PointPlateGenerator(ToleranceSettings.Default).Generate(parameters, new PointPlatePresets(dwellingUnit, stair)));
}
}
public sealed class WingedBandComponent : TypologyGeneratorComponent
{
public WingedBandComponent()
: base(
"Winged Band",
"WBand",
"Projecting wings joined by a common band (HDHM-TYP-003, ADR-006): a band of stair bays with wings of stair bays projecting to plan north, each joined to the band along its whole root; the comb outline gives the band short façade fragments between the wings. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("e285811d-2fdb-4d31-8b02-62c270b609cf");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
WingedBandParameters d = WingedBandParameters.Default;
pManager.AddIntegerParameter("Wing Count", "WC", "Number of wings projecting from the band; at least 2.", GH_ParamAccess.item, d.WingCount);
pManager.AddNumberParameter("Wing Length", "WL", "Length of each wing from the band to its tip, m.", GH_ParamAccess.item, d.WingLength);
pManager.AddNumberParameter("Wing Spacing", "WS", "Clear distance between neighbouring wings along the band, m.", GH_ParamAccess.item, d.WingSpacing);
pManager.AddNumberParameter("Depth", "D", "Depth of the band and of every wing, which every dwelling and stair spans, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Unit Width", "UW", "Target dwelling width, m; adjusted so the stair bays fill the band and each wing exactly.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of each stair along the band and the wings, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the stairs");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
int wings = 0;
if (!DA.GetData(0, ref wings) || !TryNumbers(DA, 1, 7, out double[] v) || Preset(DA, 8) is not { } dwellingUnit || Preset(DA, 9) is not { } stair)
{
return;
}
var parameters = new WingedBandParameters(wings, v[0], v[1], v[2], v[3], v[4], v[5], v[6]);
Output(DA, new WingedBandGenerator(ToleranceSettings.Default).Generate(parameters, new WingedBandPresets(dwellingUnit, stair)));
}
}
public sealed class OpenCourtComponent : TypologyGeneratorComponent
{
public OpenCourtComponent()
: base(
"Open Court",
"OCourt",
"Open U-shaped court with stair-served wings (HDHM-TYP-004, ADR-006): west, east, and north wings of stair bays around a court open to plan south, every dwelling adjoining a stair. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("223014ea-3c01-4d2b-b8ba-92ebdc4a7327");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
OpenCourtParameters d = OpenCourtParameters.Default;
pManager.AddNumberParameter("Court Width", "CW", "Width of the court between the side wings, m.", GH_ParamAccess.item, d.CourtWidth);
pManager.AddNumberParameter("Court Depth", "CD", "Depth of the court from its open side to the north wing, which is also the side wings' length, m.", GH_ParamAccess.item, d.CourtDepth);
pManager.AddNumberParameter("Depth", "D", "Depth of every wing, which every dwelling and stair spans, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Unit Width", "UW", "Target dwelling width, m; adjusted so the stair bays fill each wing exactly.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of each stair along the wings, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the stairs");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 7, out double[] v) || Preset(DA, 7) is not { } dwellingUnit || Preset(DA, 8) is not { } stair)
{
return;
}
var parameters = new OpenCourtParameters(v[0], v[1], v[2], v[3], v[4], v[5], v[6]);
Output(DA, new OpenCourtGenerator(ToleranceSettings.Default).Generate(parameters, new OpenCourtPresets(dwellingUnit, stair)));
}
}
public sealed class EnclosedCourtComponent : TypologyGeneratorComponent
{
public EnclosedCourtComponent()
: base(
"Enclosed Court",
"ECourt",
"Enclosed courtyard residential block (HDHM-TYP-005, ADR-006): south, north, west, and east wings of stair bays around one court closed on all four sides. The footprint has the court as a hole; the court façades are outdoor walls with windows. One zone per dwelling unit.")
{
}
public override Guid ComponentGuid => new("b9765cd0-a62b-483d-9470-86d5fda287e8");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
EnclosedCourtParameters d = EnclosedCourtParameters.Default;
pManager.AddNumberParameter("Court Width", "CW", "Width of the court between the west and east wings, m.", GH_ParamAccess.item, d.CourtWidth);
pManager.AddNumberParameter("Court Depth", "CD", "Depth of the court between the south and north wings, which is also the west and east wings' length, m.", GH_ParamAccess.item, d.CourtDepth);
pManager.AddNumberParameter("Depth", "D", "Depth of every wing, which every dwelling and stair spans, m.", GH_ParamAccess.item, d.Depth);
pManager.AddNumberParameter("Unit Width", "UW", "Target dwelling width, m; adjusted so the stair bays fill each wing exactly.", GH_ParamAccess.item, d.UnitWidth);
pManager.AddNumberParameter("Stair Width", "SW", "Width of each stair along the wings, m.", GH_ParamAccess.item, d.StairWidth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Dwelling Unit", "DU", SpaceType.DwellingUnit, "the dwellings");
AddPreset(pManager, "Stair", "St", SpaceType.Stair, "the stairs");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 7, out double[] v) || Preset(DA, 7) is not { } dwellingUnit || Preset(DA, 8) is not { } stair)
{
return;
}
var parameters = new EnclosedCourtParameters(v[0], v[1], v[2], v[3], v[4], v[5], v[6]);
Output(DA, new EnclosedCourtGenerator(ToleranceSettings.Default).Generate(parameters, new EnclosedCourtPresets(dwellingUnit, stair)));
}
}
public sealed class OfficePlateComponent : TypologyGeneratorComponent
{
public OfficePlateComponent()
: base(
"Office Plate",
"OPlate",
"Central service core within an office plate (SYN-TYP-017, ADR-006): a rectangular plate whose office work area is one zone, a ring around a central Core zone without outdoor walls; the core leaves at least 3 m of work area on every side. One zone per department (D-097).")
{
}
public override Guid ComponentGuid => new("783cefaa-b836-4dbc-b16d-8e74820ed361");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
OfficePlateParameters d = OfficePlateParameters.Default;
pManager.AddNumberParameter("Plate Length", "L", "Plate length along plan x, m.", GH_ParamAccess.item, d.PlateLength);
pManager.AddNumberParameter("Plate Depth", "D", "Plate depth along plan y, m.", GH_ParamAccess.item, d.PlateDepth);
pManager.AddNumberParameter("Core Length", "CL", "Length of the central core along plan x, m.", GH_ParamAccess.item, d.CoreLength);
pManager.AddNumberParameter("Core Depth", "CD", "Depth of the central core along plan y, m.", GH_ParamAccess.item, d.CoreDepth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Office", "Of", SpaceType.Office, "the office work area");
AddPreset(pManager, "Core", "Co", SpaceType.Core, "the service core");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 6, out double[] v) || Preset(DA, 6) is not { } office || Preset(DA, 7) is not { } core)
{
return;
}
var parameters = new OfficePlateParameters(v[0], v[1], v[2], v[3], v[4], v[5]);
Output(DA, new OfficePlateGenerator(ToleranceSettings.Default).Generate(parameters, new OfficePlatePresets(office, core)));
}
}
public sealed class CafeteriaComponent : TypologyGeneratorComponent
{
public CafeteriaComponent()
: base(
"Cafeteria",
"Cafe",
"Cafeteria service edge beside a dining field (SYN-TYP-016, ADR-006): the kitchen and servery, one zone, along the whole north edge of a larger dining field; the servery counter and the customers' route along it are not zones of their own. One zone per department (D-097).")
{
}
public override Guid ComponentGuid => new("95956724-ddbc-48ce-9874-0ec6c244c8fe");
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
CafeteriaParameters d = CafeteriaParameters.Default;
pManager.AddNumberParameter("Length", "L", "Length of the service edge and the dining field along plan x, m.", GH_ParamAccess.item, d.Length);
pManager.AddNumberParameter("Dining Depth", "DD", "Depth of the dining field along plan y, m.", GH_ParamAccess.item, d.DiningDepth);
pManager.AddNumberParameter("Kitchen Depth", "KD", "Depth of the kitchen and servery along plan y, m; smaller than Dining Depth.", GH_ParamAccess.item, d.KitchenDepth);
pManager.AddNumberParameter("Floor Height", "FH", FloorHeightDescription, GH_ParamAccess.item, d.FloorHeight);
pManager.AddNumberParameter("Orientation", "O", OrientationDescription, GH_ParamAccess.item, d.OrientationDegrees);
AddPreset(pManager, "Kitchen", "Ki", SpaceType.Kitchen, "the kitchen and servery");
AddPreset(pManager, "Dining", "Di", SpaceType.Dining, "the dining field");
pManager.AddParameter(ComponentSupport.PreviewLocationParameter());
}
protected override void SolveInstance(IGH_DataAccess DA)
{
if (!TryNumbers(DA, 0, 5, out double[] v) || Preset(DA, 5) is not { } kitchen || Preset(DA, 6) is not { } dining)
{
return;
}
var parameters = new CafeteriaParameters(v[0], v[1], v[2], v[3], v[4]);
Output(DA, new CafeteriaGenerator(ToleranceSettings.Default).Generate(parameters, new CafeteriaPresets(kitchen, dining)));
}
}
- [x] Step 2: Build and see the icon script fail
dotnet build BEMGen.sln -c Release
python scripts/make_icons.py
git status --short
Expected: Build succeeded., 0 Warning(s), 0 Error(s); the script stops with exit code 1 and the message Classes without an icon: ['CafeteriaComponent', 'OfficePlateComponent']; icons without a class: [], and writes nothing (git status --short lists only M src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs).
- [x] Step 3: Replace
scripts/make_icons.py(full file)
The two motifs are added after enclosed_court, and their entries after Enclosed Court in the Generate group; every other motif and entry is unchanged.
"""Draws the BEMGen Grasshopper icons (D-063).
Every icon is drawn at 96 x 96 pixels on a transparent background and downsampled to 24 x 24 with LANCZOS, then written to
src/Lod.Grasshopper/Icons/<Name>.png, where <Name> is the class name of the component, parameter, or assembly info that shows it.
Each icon is a dark glyph plus the accent colour of its toolbar panel; a parameter uses the panel of the component that makes its object.
Shapes only, no fonts, so the output does not depend on installed fonts.
Run from the repository root: python scripts/make_icons.py
Requires Python 3.10+ and Pillow. Generated with Pillow 12.3.0; the same Pillow version gives the same pixels.
The script stops with an error when an icon has no class in src/Lod.Grasshopper or a class has no icon.
"""
from __future__ import annotations
import re
import sys
from collections.abc import Callable
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / "src" / "Lod.Grasshopper"
OUTPUT = SOURCE / "Icons"
CANVAS = 96
SIZE = 24
LINE = 8 # 2 px after downsampling
DARK = (38, 38, 38, 255)
WHITE = (255, 255, 255, 255)
# One accent per toolbar panel (ComponentCategories).
INFO = (96, 125, 139, 255)
PROGRAM = (230, 140, 30, 255)
GENERATE = (46, 158, 79, 255)
SIMPLIFY = (47, 123, 216, 255)
AGGREGATE = (138, 79, 200, 255)
CONVERT = (208, 69, 58, 255)
INSPECT = (24, 150, 150, 255)
Colour = tuple[int, int, int, int]
Draw = ImageDraw.ImageDraw
Motif = Callable[[Draw, Colour], None]
Painter = Callable[[Image.Image, Colour], None]
def box(d: Draw, xy: tuple[float, float, float, float], fill=None, outline=DARK, width: int = LINE) -> None:
d.rectangle(xy, fill=fill, outline=outline, width=width)
def line(d: Draw, points: list[tuple[float, float]], width: int = LINE, fill=DARK) -> None:
d.line(points, fill=fill, width=width, joint="curve")
def arrow_head(d: Draw, tip: tuple[float, float], direction: str, size: float, fill=DARK) -> None:
x, y = tip
if direction == "right":
d.polygon([(x, y), (x - size, y - size), (x - size, y + size)], fill=fill)
elif direction == "down":
d.polygon([(x, y), (x - size, y - size), (x + size, y - size)], fill=fill)
# --- motifs: each draws inside the 96 x 96 canvas -------------------------------------------------------------
def info(d: Draw, accent) -> None:
d.ellipse((6, 6, 90, 90), fill=accent, outline=DARK, width=LINE)
d.rectangle((42, 40, 54, 74), fill=WHITE)
d.rectangle((42, 20, 54, 32), fill=WHITE)
def assembly(d: Draw, accent) -> None:
# A stepped block: the building at three levels of detail.
box(d, (8, 52, 88, 88), fill=accent)
box(d, (8, 30, 60, 56), fill=accent)
box(d, (8, 8, 36, 34), fill=accent)
def schedule(d: Draw, accent) -> None:
# A 24-hour profile as bars on a time axis.
heights = [16, 48, 72, 36, 60, 76, 24]
for i, h in enumerate(heights):
x = 12 + i * 12
d.rectangle((x, 84 - h, x + 7, 83), fill=accent)
d.rectangle((4, 84, 92, 91), fill=DARK)
d.rectangle((4, 4, 11, 91), fill=DARK)
def load(d: Draw, accent) -> None:
bolt = [(58, 4), (18, 54), (44, 54), (34, 92), (78, 38), (52, 38), (64, 4)]
d.polygon(bolt, fill=accent, outline=DARK, width=6)
def preset_tag(d: Draw, accent) -> None:
tag = [(8, 30), (30, 8), (88, 8), (88, 88), (30, 88), (8, 66)]
d.polygon(tag, fill=accent, outline=DARK, width=LINE)
d.ellipse((24, 40, 40, 56), fill=WHITE, outline=DARK, width=4)
line(d, [(52, 34), (76, 34)], width=6)
line(d, [(52, 48), (76, 48)], width=6)
line(d, [(52, 62), (76, 62)], width=6)
def house(d: Draw, accent) -> None:
d.polygon([(48, 6), (90, 44), (6, 44)], fill=accent, outline=DARK, width=LINE)
box(d, (16, 42, 80, 90), fill=accent)
d.rectangle((40, 60, 56, 90), fill=DARK)
def corridor(d: Draw, accent) -> None:
box(d, (4, 30, 92, 66), fill=accent)
line(d, [(16, 48), (60, 48)], width=8)
arrow_head(d, (78, 48), "right", 12)
def stair(d: Draw, accent) -> None:
steps = [(6, 90), (6, 70), (28, 70), (28, 50), (50, 50), (50, 30), (72, 30), (72, 10), (90, 10), (90, 90)]
d.polygon(steps, fill=accent, outline=DARK, width=LINE)
def office(d: Draw, accent) -> None:
# An office block in elevation with a grid of windows.
box(d, (16, 6, 80, 90), fill=accent)
for y in (18, 38, 58):
for x in (26, 52):
d.rectangle((x, y, x + 16, y + 12), fill=DARK)
d.rectangle((40, 76, 56, 90), fill=DARK)
def core(d: Draw, accent) -> None:
# A service core: a box with two lift doors and an up and down arrow.
box(d, (8, 8, 88, 88), fill=accent)
d.rectangle((20, 34, 42, 80), fill=DARK)
d.rectangle((54, 34, 76, 80), fill=DARK)
d.polygon([(48, 12), (38, 26), (58, 26)], fill=DARK)
def retail(d: Draw, accent) -> None:
# A shopping bag with its handle.
d.arc((30, 6, 66, 46), start=180, end=360, fill=DARK, width=LINE)
d.polygon([(14, 30), (82, 30), (90, 90), (6, 90)], fill=accent, outline=DARK, width=LINE)
def mall(d: Draw, accent) -> None:
# A branching mall in plan: shop blocks (accent) along a T of public routes (white).
box(d, (4, 4, 92, 92), fill=accent)
d.rectangle((40, 8, 56, 88), fill=WHITE)
d.rectangle((8, 40, 40, 56), fill=WHITE)
for a, b in [((40, 4), (40, 40)), ((56, 4), (56, 92)), ((40, 56), (40, 92)), ((4, 40), (40, 40)), ((4, 56), (40, 56))]:
line(d, [a, b], width=4)
def kitchen(d: Draw, accent) -> None:
# A cooking pot with a lid and two handles.
box(d, (16, 40, 80, 86), fill=accent)
line(d, [(4, 50), (16, 50)])
line(d, [(80, 50), (92, 50)])
d.rectangle((12, 28, 84, 38), fill=DARK)
d.rectangle((42, 16, 54, 28), fill=DARK)
def dining(d: Draw, accent) -> None:
# A plate between a fork and a knife.
d.ellipse((26, 22, 74, 70), fill=accent, outline=DARK, width=LINE)
d.ellipse((38, 34, 62, 58), outline=DARK, width=4)
line(d, [(12, 14), (12, 86)], width=6)
line(d, [(84, 14), (84, 86)], width=6)
d.rectangle((80, 14, 88, 46), fill=DARK)
def operating_theatre(d: Draw, accent) -> None:
# A medical cross.
d.polygon(
[(34, 6), (62, 6), (62, 34), (90, 34), (90, 62), (62, 62), (62, 90), (34, 90), (34, 62), (6, 62), (6, 34), (34, 34)],
fill=accent,
outline=DARK,
width=LINE,
)
def clean_corridor(d: Draw, accent) -> None:
# A corridor with a white (clean) route and an arrow.
box(d, (4, 30, 92, 66), fill=accent)
d.rectangle((10, 42, 70, 54), fill=WHITE)
arrow_head(d, (86, 48), "right", 12)
def dirty_corridor(d: Draw, accent) -> None:
# A corridor with a hatched (dirty) route and an arrow.
box(d, (4, 30, 92, 66), fill=accent)
for x in range(12, 66, 14):
line(d, [(x, 60), (x + 10, 36)], width=5)
arrow_head(d, (86, 48), "right", 12)
def clinical_support(d: Draw, accent) -> None:
# Shelves of supplies beside a small medical cross.
box(d, (6, 10, 58, 90), fill=accent)
for y in (34, 58):
line(d, [(6, y), (58, y)], width=6)
d.rectangle((70, 28, 82, 64), fill=DARK)
d.rectangle((58, 40, 94, 52), fill=DARK)
def care_bedroom(d: Draw, accent) -> None:
# A bed in elevation: headboard, mattress, and pillow.
d.rectangle((6, 24, 16, 86), fill=DARK)
box(d, (16, 54, 90, 72), fill=accent)
d.rectangle((20, 42, 42, 54), fill=DARK)
line(d, [(86, 72), (86, 86)])
def care_communal(d: Draw, accent) -> None:
# A round table with four people around it.
d.ellipse((28, 28, 68, 68), fill=accent, outline=DARK, width=LINE)
for x, y in [(48, 12), (84, 48), (48, 84), (12, 48)]:
d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=DARK)
def lobby(d: Draw, accent) -> None:
# A glazed entrance front with an open door and an arrow leading in.
box(d, (6, 10, 90, 88), fill=accent)
line(d, [(34, 10), (34, 88)], width=4)
line(d, [(62, 10), (62, 88)], width=4)
d.rectangle((38, 44, 58, 88), fill=WHITE, outline=DARK, width=4)
arrow_head(d, (48, 84), "down", 8)
def activity_hall(d: Draw, accent) -> None:
# A sports court in plan: the hall, its centre line, and the centre circle.
box(d, (4, 16, 92, 80), fill=accent)
line(d, [(48, 16), (48, 80)], width=5)
d.ellipse((34, 34, 62, 62), outline=DARK, width=5)
def service_rooms(d: Draw, accent) -> None:
# A row of three lockers with their handles.
for left in (6, 36, 66):
box(d, (left, 8, left + 24, 88), fill=accent, width=6)
d.rectangle((left + 15, 40, left + 19, 56), fill=DARK)
def linear_generator(d: Draw, accent) -> None:
# Footprint with a west stair, a corridor band, and dwelling-unit divisions on both sides.
box(d, (4, 12, 91, 83))
d.rectangle((12, 20, 23, 75), fill=DARK)
d.rectangle((28, 40, 87, 55), fill=accent)
for x in (48, 68):
line(d, [(x, 12), (x, 40)])
line(d, [(x, 55), (x, 83)])
def stair_bay_bar(d: Draw, accent) -> None:
# A long bar of two stair bays: dwelling (accent) | stair (dark) | dwelling, repeated, with a party wall between the bays.
box(d, (2, 30, 93, 66), fill=accent)
for x in (20, 66):
d.rectangle((x, 30, x + 10, 66), fill=DARK)
line(d, [(48, 30), (48, 66)], width=4)
def stair_pair(d: Draw, accent) -> None:
# One compact plate: two dwellings (accent) around one full-depth stair (dark).
box(d, (14, 22, 82, 74), fill=accent)
d.rectangle((42, 26, 54, 70), fill=DARK)
def gallery_bar(d: Draw, accent) -> None:
# A bar of dwellings (accent) between two end stairs (dark); the gallery along it is dashed, as it is not modelled.
box(d, (4, 34, 91, 76), fill=accent)
d.rectangle((8, 38, 18, 72), fill=DARK)
d.rectangle((77, 38, 87, 72), fill=DARK)
for x in (36, 54):
line(d, [(x, 34), (x, 76)], width=6)
for x in range(6, 90, 16):
line(d, [(x, 20), (x + 9, 20)], width=6)
def terrace_row(d: Draw, accent) -> None:
# Attached houses in elevation: three gables sharing their side walls.
houses = [(6, 34), (34, 62), (62, 90)]
for left, right in houses:
middle = (left + right) / 2
d.polygon([(left, 46), (middle, 22), (right, 46), (right, 88), (left, 88)], fill=accent, outline=DARK, width=6)
for left, right in houses:
d.rectangle((left + 10, 68, right - 10, 88), fill=DARK)
def point_plate(d: Draw, accent) -> None:
# A square plate: four dwellings (accent) in a pinwheel around one central core (dark) without façades.
box(d, (6, 6, 90, 90), fill=accent)
d.rectangle((36, 36, 60, 60), fill=DARK)
for a, b in [((6, 36), (60, 36)), ((60, 6), (60, 60)), ((36, 60), (90, 60)), ((36, 36), (36, 90))]:
line(d, [a, b], width=6)
def winged_band(d: Draw, accent) -> None:
# A comb in plan: a band (bottom) with three wings projecting north (up), every wing joined to the band along its root; stairs dark.
d.polygon(
[(4, 90), (92, 90), (92, 8), (70, 8), (70, 62), (59, 62), (59, 8), (37, 8), (37, 62), (26, 62), (26, 8), (4, 8)],
fill=accent,
outline=DARK,
width=6,
)
for left, right in ((4, 26), (37, 59), (70, 92)):
d.rectangle((left + 3, 30, right - 3, 38), fill=DARK)
for x in (20, 48, 76):
d.rectangle((x, 66, x + 6, 87), fill=DARK)
def open_court(d: Draw, accent) -> None:
# A U in plan: a north wing (top) across the full width and two side wings, around a court open to the south (bottom); stairs dark.
d.polygon([(4, 92), (28, 92), (28, 34), (68, 34), (68, 92), (92, 92), (92, 6), (4, 6)], fill=accent, outline=DARK, width=6)
for x in (24, 45, 66):
d.rectangle((x, 9, x + 6, 31), fill=DARK)
for left in (4, 68):
d.rectangle((left + 3, 58, left + 21, 66), fill=DARK)
def enclosed_court(d: Draw, accent) -> None:
# A closed ring in plan: south and north wings across the full width and west and east wings between them, around a court (the hole,
# transparent) closed on all four sides; stairs dark.
box(d, (4, 4, 92, 92), fill=accent, width=6)
box(d, (30, 30, 66, 66), fill=(0, 0, 0, 0), width=6)
for y in (7, 69):
for x in (24, 66):
d.rectangle((x, y, x + 6, y + 20), fill=DARK)
for left in (7, 69):
d.rectangle((left, 45, left + 20, 51), fill=DARK)
def office_plate(d: Draw, accent) -> None:
# A rectangular plate: one office work area (accent) all around a central service core (dark).
box(d, (4, 16, 92, 80), fill=accent)
d.rectangle((32, 40, 64, 56), fill=DARK)
def cafeteria(d: Draw, accent) -> None:
# A rectangle: the kitchen and servery along the north edge (dark) and the dining field (accent) with tables.
box(d, (4, 10, 92, 86), fill=accent)
d.rectangle((8, 14, 88, 34), fill=DARK)
for y in (48, 68):
for x in (20, 44, 68):
d.ellipse((x, y, x + 10, y + 10), fill=DARK)
def transform_plan(d: Draw, accent) -> None:
# A footprint outline and the same footprint rotated and moved, joined by a turning arrow.
box(d, (6, 52, 50, 90), width=6)
d.polygon([(66, 12), (92, 28), (76, 56), (50, 40)], fill=accent, outline=DARK, width=6)
d.arc((18, 20, 70, 72), start=180, end=270, fill=DARK, width=8)
arrow_head(d, (58, 24), "right", 10)
def z0(d: Draw, accent) -> None:
# Many cells: every unit its own zone.
box(d, (4, 4, 91, 91), fill=accent)
for k in (32, 60):
line(d, [(k, 4), (k, 91)])
line(d, [(4, k), (91, k)])
def z1(d: Draw, accent) -> None:
# Cells of one space type merged: two unit rows and a corridor.
box(d, (4, 10, 92, 40), fill=accent)
box(d, (4, 56, 92, 86), fill=accent)
box(d, (4, 36, 92, 60))
def z2(d: Draw, accent) -> None:
# Perimeter ring split on the corner bisectors, plus a core.
box(d, (4, 4, 92, 92))
d.rectangle((30, 30, 66, 66), fill=accent, outline=DARK, width=LINE)
for a, b in [((4, 4), (30, 30)), ((92, 4), (66, 30)), ((4, 92), (30, 66)), ((92, 92), (66, 66))]:
line(d, [a, b], width=6)
def z3(d: Draw, accent) -> None:
box(d, (6, 16, 90, 80), fill=accent)
def slab(d: Draw, accent, y: float, height: float = 16, left: float = 6, right: float = 90) -> None:
box(d, (left, y, right, y + height), fill=accent, width=6)
def stack(d: Draw, accent) -> None:
for y in (8, 36, 64):
slab(d, accent, y, 24)
def multiplier(d: Draw, accent) -> None:
slab(d, accent, 6, 24, 6, 90)
line(d, [(18, 44), (50, 84)], width=10)
line(d, [(50, 44), (18, 84)], width=10)
for y in (44, 58, 72):
d.rectangle((62, y, 90, y + 8), fill=DARK)
def single_zone_per_floor_type(d: Draw, accent) -> None:
# One zone per floor entry: a one-storey zone below a three-storey zone.
box(d, (24, 66, 71, 91), fill=accent)
box(d, (24, 4, 71, 58), fill=accent)
def single_zone_building(d: Draw, accent) -> None:
box(d, (24, 4, 71, 91), fill=accent)
def convert(d: Draw, accent) -> None:
d.polygon([(40, 44), (90, 44), (90, 92), (40, 92)], fill=accent, outline=DARK, width=LINE)
line(d, [(6, 14), (44, 14), (64, 34), (64, 56)], width=10)
arrow_head(d, (64, 74), "down", 16)
def inspect(d: Draw, accent) -> None:
d.ellipse((6, 6, 64, 64), fill=accent, outline=DARK, width=LINE)
d.ellipse((18, 18, 38, 38), fill=WHITE)
line(d, [(56, 56), (88, 88)], width=16)
def mini_grid(d: Draw, origin: tuple[float, float], size: float, cells: int, accent) -> None:
x0, y0 = origin
box(d, (x0, y0, x0 + size, y0 + size), fill=accent, width=6)
step = size / cells
for k in range(1, cells):
line(d, [(x0 + k * step, y0), (x0 + k * step, y0 + size)], width=4)
line(d, [(x0, y0 + k * step), (x0 + size, y0 + k * step)], width=4)
def map_source_target(d: Draw, accent) -> None:
mini_grid(d, (4, 4), 40, 2, accent)
box(d, (52, 52, 92, 92), fill=accent, width=6)
line(d, [(32, 56), (32, 72), (40, 72)], width=8)
arrow_head(d, (52, 72), "right", 12)
line(d, [(56, 26), (72, 26), (72, 36)], width=8)
arrow_head(d, (72, 50), "down", 12)
def validate(d: Draw, accent) -> None:
d.ellipse((4, 4, 92, 92), fill=accent, outline=DARK, width=LINE)
line(d, [(24, 50), (42, 68), (74, 30)], width=14, fill=WHITE)
# Parameter motifs: small versions of their object's motif, drawn inside the hexagon (x and y in 20..76).
def small_schedule(d: Draw, accent) -> None:
for x, h in ((28, 16), (40, 36), (52, 24), (64, 40)):
d.rectangle((x, 72 - h, x + 7, 71), fill=accent)
d.rectangle((24, 72, 75, 75), fill=DARK)
def small_load(d: Draw, accent) -> None:
d.polygon([(54, 20), (30, 52), (46, 52), (40, 76), (68, 44), (52, 44), (60, 20)], fill=accent, outline=DARK, width=4)
def small_preset(d: Draw, accent) -> None:
d.polygon([(20, 36), (32, 24), (76, 24), (76, 72), (32, 72), (20, 60)], fill=accent, outline=DARK, width=4)
d.ellipse((30, 42, 42, 54), fill=WHITE, outline=DARK, width=4)
def small_plan(d: Draw, accent) -> None:
box(d, (20, 24, 75, 71), width=4)
d.rectangle((24, 44, 71, 51), fill=accent)
d.rectangle((24, 28, 31, 67), fill=DARK)
for x in (48,):
line(d, [(x, 24), (x, 44)], width=4)
line(d, [(x, 52), (x, 71)], width=4)
def small_floor(d: Draw, accent) -> None:
box(d, (16, 36, 79, 59), fill=accent, width=4)
def small_building(d: Draw, accent) -> None:
box(d, (32, 16, 63, 79), fill=accent, width=4)
for y in (36, 56):
line(d, [(32, y), (63, y)], width=4)
# --- icons ----------------------------------------------------------------------------------------------------
def component(motif: Motif) -> Painter:
"""A component: its motif at full size."""
def paint(image: Image.Image, accent: Colour) -> None:
motif(ImageDraw.Draw(image), accent)
return paint
def parameter(motif: Motif) -> Painter:
"""A parameter: a small version of its object's motif inside a hexagon (the Grasshopper parameter shape)."""
def paint(image: Image.Image, accent: Colour) -> None:
d = ImageDraw.Draw(image)
d.polygon([(24, 4), (72, 4), (94, 48), (72, 92), (24, 92), (2, 48)], fill=WHITE, outline=DARK, width=4)
motif(d, accent)
return paint
ICONS: dict[str, tuple[Colour, Painter]] = {
"BemGenAssemblyInfo": (INFO, component(assembly)),
"BemGenInfoComponent": (INFO, component(info)),
"ScheduleComponent": (PROGRAM, component(schedule)),
"LoadComponent": (PROGRAM, component(load)),
"ProgramPresetComponent": (PROGRAM, component(preset_tag)),
"DwellingUnitPresetComponent": (PROGRAM, component(house)),
"CorridorPresetComponent": (PROGRAM, component(corridor)),
"StairPresetComponent": (PROGRAM, component(stair)),
"OfficePresetComponent": (PROGRAM, component(office)),
"CorePresetComponent": (PROGRAM, component(core)),
"RetailPresetComponent": (PROGRAM, component(retail)),
"MallPresetComponent": (PROGRAM, component(mall)),
"KitchenPresetComponent": (PROGRAM, component(kitchen)),
"DiningPresetComponent": (PROGRAM, component(dining)),
"OperatingTheatrePresetComponent": (PROGRAM, component(operating_theatre)),
"CleanCorridorPresetComponent": (PROGRAM, component(clean_corridor)),
"DirtyCorridorPresetComponent": (PROGRAM, component(dirty_corridor)),
"ClinicalSupportPresetComponent": (PROGRAM, component(clinical_support)),
"CareBedroomPresetComponent": (PROGRAM, component(care_bedroom)),
"CareCommunalPresetComponent": (PROGRAM, component(care_communal)),
"LobbyPresetComponent": (PROGRAM, component(lobby)),
"ActivityHallPresetComponent": (PROGRAM, component(activity_hall)),
"ServicePresetComponent": (PROGRAM, component(service_rooms)),
"LinearPlanGeneratorComponent": (GENERATE, component(linear_generator)),
"StairBayBarComponent": (GENERATE, component(stair_bay_bar)),
"StairPairComponent": (GENERATE, component(stair_pair)),
"GalleryBarComponent": (GENERATE, component(gallery_bar)),
"TerraceRowComponent": (GENERATE, component(terrace_row)),
"PointPlateComponent": (GENERATE, component(point_plate)),
"WingedBandComponent": (GENERATE, component(winged_band)),
"OpenCourtComponent": (GENERATE, component(open_court)),
"EnclosedCourtComponent": (GENERATE, component(enclosed_court)),
"OfficePlateComponent": (GENERATE, component(office_plate)),
"CafeteriaComponent": (GENERATE, component(cafeteria)),
"TransformPlanComponent": (GENERATE, component(transform_plan)),
"NoSimplificationComponent": (SIMPLIFY, component(z0)),
"SemanticMergeComponent": (SIMPLIFY, component(z1)),
"PerimeterCoreComponent": (SIMPLIFY, component(z2)),
"SingleZonePerFloorComponent": (SIMPLIFY, component(z3)),
"StackFloorsComponent": (AGGREGATE, component(stack)),
"StackedFloorZoneMultiplierComponent": (AGGREGATE, component(multiplier)),
"SingleZonePerFloorTypeComponent": (AGGREGATE, component(single_zone_per_floor_type)),
"SingleZoneBuildingComponent": (AGGREGATE, component(single_zone_building)),
"Convert2BemComponent": (CONVERT, component(convert)),
"InspectComponent": (INSPECT, component(inspect)),
"MapSourceToTargetComponent": (INSPECT, component(map_source_target)),
"ValidateComponent": (INSPECT, component(validate)),
"ScheduleParameter": (PROGRAM, parameter(small_schedule)),
"LoadParameter": (PROGRAM, parameter(small_load)),
"ProgramPresetParameter": (PROGRAM, parameter(small_preset)),
"PlanParameter": (GENERATE, parameter(small_plan)),
"FloorParameter": (SIMPLIFY, parameter(small_floor)),
"BuildingParameter": (AGGREGATE, parameter(small_building)),
}
def render(name: str) -> Image.Image:
accent, paint = ICONS[name]
image = Image.new("RGBA", (CANVAS, CANVAS), (0, 0, 0, 0))
paint(image, accent)
return image.resize((SIZE, SIZE), Image.Resampling.LANCZOS)
def classes_with_icons() -> set[str]:
"""Concrete classes that show an icon: components, parameters, and the assembly info."""
pattern = re.compile(r"public sealed class (\w+) : (\w+)")
names = set()
for path in SOURCE.rglob("*.cs"):
if "obj" in path.parts or "bin" in path.parts:
continue
for name, _ in pattern.findall(path.read_text(encoding="utf-8")):
if name.endswith(("Component", "Parameter")) or name == "BemGenAssemblyInfo":
names.add(name)
return names
def main() -> int:
expected = classes_with_icons()
missing = sorted(expected - ICONS.keys())
unknown = sorted(ICONS.keys() - expected)
if missing or unknown:
print(f"Classes without an icon: {missing}; icons without a class: {unknown}", file=sys.stderr)
return 1
OUTPUT.mkdir(parents=True, exist_ok=True)
stale = sorted(p.name for p in OUTPUT.glob("*.png") if p.stem not in ICONS)
if stale:
print(f"Icons without a class in {OUTPUT}: {stale}", file=sys.stderr)
return 1
for name in sorted(ICONS):
render(name).save(OUTPUT / f"{name}.png", optimize=False)
print(f"Wrote {len(ICONS)} icons to {OUTPUT.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
sys.exit(main())
- [x] Step 4: Draw the icons and check the hashes
python scripts/make_icons.py
git status --short
sha256sum src/Lod.Grasshopper/Icons/*.png
Expected: Wrote 53 icons to src\Lod.Grasshopper\Icons; git status --short shows M scripts/make_icons.py, the file of Step 1, and the two new PNGs, and no existing PNG modified; the hashes (Pillow 12.3.0):
a4e0709b6d8cd05a79c33af9ee864918850e74c8aef2c3ac617859fc08ac2fdc *src/Lod.Grasshopper/Icons/ActivityHallPresetComponent.png
606a11f2bbd6ee6cb7edede403166ae0f1b39231fcf5882dff360717df990cae *src/Lod.Grasshopper/Icons/BemGenAssemblyInfo.png
e33ae284b7f017ee11851986e79e4b8449e1b5027ffedf41cad68ed2abbfb15d *src/Lod.Grasshopper/Icons/BemGenInfoComponent.png
431104585e8745167567797eefef526bbf664ba6777101e3677281cb6976bfde *src/Lod.Grasshopper/Icons/BuildingParameter.png
d0a134b08a035f0ca5608468fd9f12804fd21db1a79a8bd3d19ee1b1ac88bd0a *src/Lod.Grasshopper/Icons/CafeteriaComponent.png
9805fc04d927c05f955ef5dfd0e6913e247bfec90c495a8adf051c36637cf866 *src/Lod.Grasshopper/Icons/CareBedroomPresetComponent.png
88900c3c46841a23766ace6b7a1d22a5a3bbb4e6403c2adc71af1fd3c404ff92 *src/Lod.Grasshopper/Icons/CareCommunalPresetComponent.png
516ecd2a3fbb009d1b541e392965c78a85bdec71b5a6a99b71caf71628cf31f3 *src/Lod.Grasshopper/Icons/CleanCorridorPresetComponent.png
69d26c3e73a28fc34768fdb14ada2e202940d28ec0ef159786d9be9e2ff8ebd0 *src/Lod.Grasshopper/Icons/ClinicalSupportPresetComponent.png
edc0ff9b82e6161bf8bdf3b5acebc8f4a43644d42d845827a101593d91d1a110 *src/Lod.Grasshopper/Icons/Convert2BemComponent.png
b35b3a0f58c4893203982ac209cb6004dd8a3d6264ad58dc0875a43aa6c65342 *src/Lod.Grasshopper/Icons/CorePresetComponent.png
c8a815ff220dafff9f84af05411eab8c43191b4b6f4be2f0b4750385e1d95489 *src/Lod.Grasshopper/Icons/CorridorPresetComponent.png
5e37af921cd4055aeedb4d409d5e966b6515acb880aa28e9080aabf4c593b201 *src/Lod.Grasshopper/Icons/DiningPresetComponent.png
92c7e19aa3faf6fdbf3a4f8d23f6919d18490f19ccf954f41c78efaee741ded6 *src/Lod.Grasshopper/Icons/DirtyCorridorPresetComponent.png
755de6f9d4122d567040d0df98ec3232c76f4f1c0af9da934276fe33585dc528 *src/Lod.Grasshopper/Icons/DwellingUnitPresetComponent.png
04caf35a33a9b6443b7fa808766237aecb235db70ffe0f869002d9d3287223d8 *src/Lod.Grasshopper/Icons/EnclosedCourtComponent.png
324ffc55b8f3aed81902fdaea4ac6293654a194e9f58aa97c9aa088547e584be *src/Lod.Grasshopper/Icons/FloorParameter.png
edc98c6c83c0040d935633c0e8fa014ebeeeeae5ef8f596207fd385ad09a0dbe *src/Lod.Grasshopper/Icons/GalleryBarComponent.png
a7191b8f5e30874eaf7d50f0d4555b3565b77157f83ae584393ab9c3b1c3a792 *src/Lod.Grasshopper/Icons/InspectComponent.png
15d1a772ec51f8411eaa7a61cc847b0344147dbf267badc4b61bdc774359a072 *src/Lod.Grasshopper/Icons/KitchenPresetComponent.png
75b4d98c4ad7cffbec53284781d3ad544ccd8ba762c7fd5c33bfd6d6845e42d4 *src/Lod.Grasshopper/Icons/LinearPlanGeneratorComponent.png
acf5163f017f01a1ebb394861f61750e54f9ae8c730a0a6a614f2120f59feadc *src/Lod.Grasshopper/Icons/LoadComponent.png
5045ceb77bc754553aa717110fab061978ef7b51d0c05e0f7f93788a8158f1d5 *src/Lod.Grasshopper/Icons/LoadParameter.png
197cbe6bd28af5e70cc5987020cf0f91b8bd9641afd81f78596e8be7d820a832 *src/Lod.Grasshopper/Icons/LobbyPresetComponent.png
0f1400bcff9d9791690438d777602fa9e00f8405443d8b5108eefdc1d4c5e667 *src/Lod.Grasshopper/Icons/MallPresetComponent.png
438a45a6b5f3071e4918cb6c0f7e6112e4d0ab9ca0ad1aacdc189b4e5de9ecb8 *src/Lod.Grasshopper/Icons/MapSourceToTargetComponent.png
4a702c6b6ae3c4773678a3594228ce4d1e06c2623bc9eeb5bf1fdaaee8e50c30 *src/Lod.Grasshopper/Icons/NoSimplificationComponent.png
7d8823490704b3321526b2c26d4f434f3df9810bf3d9b00d71cf569fc9aebb58 *src/Lod.Grasshopper/Icons/OfficePlateComponent.png
43e15233e02b20e102659703622e13469c5f024f6c587edc9bf2403667390b8f *src/Lod.Grasshopper/Icons/OfficePresetComponent.png
e9504a1cea0d69fe7da893cd10e220358f439b119c3ea86efe6e4d3a9cc29e3e *src/Lod.Grasshopper/Icons/OpenCourtComponent.png
c376a8fbfc51c257f0d57f1174fd46c110356261f3bbd8d895df89e76a977b49 *src/Lod.Grasshopper/Icons/OperatingTheatrePresetComponent.png
ac5d421e2cded0be0f7797510b864cdfcdd019dd46caed4147251923602184b8 *src/Lod.Grasshopper/Icons/PerimeterCoreComponent.png
a4bd3109301f4fccec8fa698ceca5dcfbb22e4dc839d850dd5aa4241b5cca95b *src/Lod.Grasshopper/Icons/PlanParameter.png
cb207d9cb3ccc6df62da1fbfc077921526b91ffb8e329ee225c97163e7014c3b *src/Lod.Grasshopper/Icons/PointPlateComponent.png
13741aac4525cf0c0d991bf3b673e55cd22612324fdc3ef3a8aaaaf0e66dec45 *src/Lod.Grasshopper/Icons/ProgramPresetComponent.png
bd0a92bf623dd4b3f6b9c4279e425adc44a7287751d97e8206a26e3c26e7e400 *src/Lod.Grasshopper/Icons/ProgramPresetParameter.png
0c84c2d19c379dfe78aea82f8ed400848665472bcc5b6159b7f1036d25af3f02 *src/Lod.Grasshopper/Icons/RetailPresetComponent.png
90646f46d781eca01dcdf67554987ee04fdd80c450e5f94647858b5f7e7b613d *src/Lod.Grasshopper/Icons/ScheduleComponent.png
693d5513ffe5f813ed3a77dc7b68cfce31e008b407e6410ac34c9ed374c6bfaa *src/Lod.Grasshopper/Icons/ScheduleParameter.png
8217bd3bbe904a6026ef7434606168b5d466341f356280a623f92278908a4068 *src/Lod.Grasshopper/Icons/SemanticMergeComponent.png
b60466bed4dd2bdcb51bc0488af40bdeb5db9f2c24d70f12d32bf1d1b147b436 *src/Lod.Grasshopper/Icons/ServicePresetComponent.png
682ee28436ffab1bc47382ff9915e27f2c774d0ef22448422d2316d739b5727f *src/Lod.Grasshopper/Icons/SingleZoneBuildingComponent.png
380ce788eef6adc20c2f45723deae73b4b747d84ffe5e2664001e46d21157c49 *src/Lod.Grasshopper/Icons/SingleZonePerFloorComponent.png
b8bb1b2890ce6ac6273f49e863b3a25ae0c05c87dc37e33b5483913e6ac52162 *src/Lod.Grasshopper/Icons/SingleZonePerFloorTypeComponent.png
5859a90fd17bb2072b1971cd31caa0df678f29bf2ad9f279c910dc59c66ca0f9 *src/Lod.Grasshopper/Icons/StackFloorsComponent.png
aa115c4ba9c51350ff291f45c6e16db41be505bc5153b6ef8159165f3286a37f *src/Lod.Grasshopper/Icons/StackedFloorZoneMultiplierComponent.png
9ffba933d6cd515b59259979ce8ce666f1a40ed906af5d5b25875f25458115f9 *src/Lod.Grasshopper/Icons/StairBayBarComponent.png
d25e67dce5eceed6cb2379c55755d38c495b4ee227069ac54bed185ed751910c *src/Lod.Grasshopper/Icons/StairPairComponent.png
939d76b08d77d2d099bd823473f1d86eb8b16983840230cf995f8ade9d0debcc *src/Lod.Grasshopper/Icons/StairPresetComponent.png
ab59722b25ff1a3407cf7344e1c4595142b7a3bed05b7f952d58f92b51ffae34 *src/Lod.Grasshopper/Icons/TerraceRowComponent.png
3ce5e835f0921d1dee8feeaf23a45aecd36c3aedb55be5a8386fdb3e540e5246 *src/Lod.Grasshopper/Icons/TransformPlanComponent.png
20a372402d7ca8ae673db6a0a1299d7a3b6dcae9141d29de2b535cc543d950ce *src/Lod.Grasshopper/Icons/ValidateComponent.png
61af5011fa771797ae48e99ba73cbb7d1cb165a2ec11784187a94dd0fbfbd704 *src/Lod.Grasshopper/Icons/WingedBandComponent.png
Run python scripts/make_icons.py a second time: the hashes do not change.
- [x] Step 5: Look at the icons
python - <<'EOF'
from pathlib import Path
from PIL import Image
icons = sorted(Path("src/Lod.Grasshopper/Icons").glob("*.png"))
cell = 24 * 8 + 16
sheet = Image.new("RGBA", (6 * cell, -(-len(icons) // 6) * cell), (235, 235, 235, 255))
for i, path in enumerate(icons):
big = Image.open(path).convert("RGBA").resize((24 * 8, 24 * 8), Image.Resampling.NEAREST)
sheet.alpha_composite(big, ((i % 6) * cell + 8, (i // 6) * cell + 8))
Path("src/Lod.Grasshopper/bin").mkdir(parents=True, exist_ok=True)
sheet.save("src/Lod.Grasshopper/bin/icon-sheet.png")
print(len(icons), "icons drawn on src/Lod.Grasshopper/bin/icon-sheet.png")
EOF
Expected: 53 icons drawn on src/Lod.Grasshopper/bin/icon-sheet.png.
Open the image: Cafeteria is the fifth icon of the first row, after Building (a green rectangle with a dark kitchen band along its north edge and dark tables in the dining field), and Office Plate the fourth icon of the fifth row, after No Simplification (a green plate around a dark central core); every icon after each of them moves one place on, the 51 icons of Task 6 keep their motifs, and the 53 icons fill eight rows of six and five icons of a ninth. If the hashes of Step 4 differed, this check decides, as in Task 6, Step 6.
- [x] Step 6: Build and check the embedded icons
dotnet build BEMGen.sln -c Release
git grep -c "IconLibrary.For(this)" -- src/Lod.Grasshopper
powershell -NoProfile -Command "([Reflection.Assembly]::ReflectionOnlyLoadFrom((Resolve-Path 'src/Lod.Grasshopper/bin/Release/net7.0/BEMGen.gha').Path)).GetManifestResourceNames().Count"
Expected: Build succeeded., 0 Warning(s), 0 Error(s); counts src/Lod.Grasshopper/BemGenAssemblyInfo.cs:1, src/Lod.Grasshopper/Components/BemGenInfoComponent.cs:1, src/Lod.Grasshopper/Components/Convert2BemComponent.cs:1, src/Lod.Grasshopper/Components/PipelineComponents.cs:4, src/Lod.Grasshopper/Components/PresetComponents.cs:1, src/Lod.Grasshopper/Components/ProgramComponents.cs:3, src/Lod.Grasshopper/Components/SimplifierComponents.cs:3, src/Lod.Grasshopper/Components/TransformPlanComponent.cs:1, src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs:1, src/Lod.Grasshopper/Parameters/BemParameters.cs:1 (the new components inherit the icon of TypologyGeneratorComponent and DefaultPresetComponent, as the other typology and preset components do); 53 resource names, one for each PNG of Step 4.
- [x] Step 7: Commit
git add src/Lod.Grasshopper/Components/TypologyGeneratorComponents.cs scripts/make_icons.py src/Lod.Grasshopper/Icons
git commit -m "feature(grasshopper): add the office plate and cafeteria generator components"
Expected before git add: the two modified files and the two new PNGs; nothing under bin/.
- [x] Step 8: Merge Slice C (D-004)
git fetch origin
git switch main
git pull --ff-only
git switch feature/office-plate-cafeteria
git rebase main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
git switch main
git merge --ff-only feature/office-plate-cafeteria
git branch -d feature/office-plate-cafeteria
git diff --name-status v0.8.2 main -- "tests/*/Snapshots/*"
Expected: VERIFY PASSED, with 193 core, 224 generators, and 704 integration tests passed on net8.0; the last command lists only
A tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt
A tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt
the sixteen existing snapshots are byte-identical. Do not push (D-098).
Slice D — docs/architecture-non-residential¶
Task 10: Architecture documents, ADR notes, and the S8.4 smoke-test checklist¶
Files:
- Modify: docs/architecture/pipeline.md, docs/architecture/convert2bem.md, docs/research/program-presets.md (last changed in S8.3), docs/architecture/domain-model.md (last changed in S8.1), docs/decisions/ADR-002-geometry.md (last changed in S8.3), docs/decisions/ADR-006-plan-generation-mechanism.md (Task 4), docs/development/grasshopper-smoke-test.md (append)
Interfaces:
- Consumes: Tasks 5–9 (the space types and presets, the generators, the components, their GUIDs and inputs).
- Produces: pipeline.md after S8.4: the non-residential presets, the two generators, the zone with a hole in a footprint without one, and the heading ## Grasshopper components through S8.4 (anchor #grasshopper-components-through-s84) with the seventeen components, which the overview of pipeline.md and program-presets.md link to; program-presets.md: the fifteen non-residential presets with their values and schedules (ADR-014); domain-model.md: the new space types and ExampleNonResidentialPresets; convert2bem.md and ADR-002: a zone with a hole whose footprint has none; ADR-006: Implemented (S8.4) for 017 and 016 and the section Pipeline consequences of S8.4; the S8.4 checklist that Task 11 runs.
- [x] Step 1: Create the branch
git switch -c docs/architecture-non-residential main
- [x] Step 2: Edit
docs/architecture/pipeline.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
At Checkpoint 1 (stages S0–S4, D-030) the pipeline is complete for the linear plan; the revision stage S4.1 (D-061 to D-064) gave the plan generator one preset input per space type and added default preset components, a *Preview Location* input, and component icons; the revision stage S4.2 (D-068 to D-075, [ADR-013](../decisions/ADR-013-vertical-aggregation-methods.md)) added *Transform Plan*, made the floor aggregators designate floors and ceilings on the assembled stack, turned them into four methods, and made heat transfer between zones a conversion option; stage S5 (D-079, [ADR-012](../decisions/ADR-012-centred-windows.md)) gives every rebuilt outdoor wall one centred window with the glazed area it covers; stage S8.1 (D-082 to D-084, [ADR-006](../decisions/ADR-006-plan-generation-mechanism.md), [ADR-011](../decisions/ADR-011-minimum-wall-and-window-geometry.md)) adds a plan generator for each bar family of the *High-Density Housing* precedent study and the minimum wall and window geometry of every generator; stage S8.2 (D-084) adds the non-rectangular families without holes, the point plate, the winged band, and the open court, and makes the united zones and the edge tests of the surface builder exact on rotated plans; stage S8.3 (D-084) adds the enclosed court, the first footprint with a hole, which every simplifier and aggregator supports. This document describes each step, the rules it applies, where results are validated, and the Grasshopper components that expose it. The domain types are described in [domain-model.md](domain-model.md), the validation checks in [validation.md](validation.md), and the `Convert2BEM` output layout in [convert2bem.md](convert2bem.md).
with
At Checkpoint 1 (stages S0–S4, D-030) the pipeline is complete for the linear plan; the revision stage S4.1 (D-061 to D-064) gave the plan generator one preset input per space type and added default preset components, a *Preview Location* input, and component icons; the revision stage S4.2 (D-068 to D-075, [ADR-013](../decisions/ADR-013-vertical-aggregation-methods.md)) added *Transform Plan*, made the floor aggregators designate floors and ceilings on the assembled stack, turned them into four methods, and made heat transfer between zones a conversion option; stage S5 (D-079, [ADR-012](../decisions/ADR-012-centred-windows.md)) gives every rebuilt outdoor wall one centred window with the glazed area it covers; stage S8.1 (D-082 to D-084, [ADR-006](../decisions/ADR-006-plan-generation-mechanism.md), [ADR-011](../decisions/ADR-011-minimum-wall-and-window-geometry.md)) adds a plan generator for each bar family of the *High-Density Housing* precedent study and the minimum wall and window geometry of every generator; stage S8.2 (D-084) adds the non-rectangular families without holes, the point plate, the winged band, and the open court, and makes the united zones and the edge tests of the surface builder exact on rotated plans; stage S8.3 (D-084) adds the enclosed court, the first footprint with a hole, which every simplifier and aggregator supports; stage S8.4 (D-093 to D-098, [ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md)) adds the non-residential space types with their presets and default preset components, and the office plate and the cafeteria of the four-source precedent set. This document describes each step, the rules it applies, where results are validated, and the Grasshopper components that expose it. The domain types are described in [domain-model.md](domain-model.md), the validation checks in [validation.md](validation.md), and the `Convert2BEM` output layout in [convert2bem.md](convert2bem.md).
- Replace
→ plan generator PlanGenerator<P, Presets>: LinearPlanGenerator | → IGeneratedPlan one preset input per space type; Z0: one zone per dwelling unit, explicit windows (ADR-011 limits)
with
→ plan generator PlanGenerator<P, Presets>: LinearPlanGenerator | → IGeneratedPlan one preset input per space type; Z0: one zone per dwelling unit or department, explicit windows (ADR-011 limits)
- Replace
EnclosedCourt
with
EnclosedCourt | OfficePlate | Cafeteria
- Replace
- All logic lives in `Lod.Core` and `Lod.Generators`, which reference neither Rhino nor Grasshopper; their tests run with `dotnet test`. `Lod.Grasshopper` wraps each pipeline type in a Goo with a viewport preview and exposes one component per step (D-017, D-018). Every component that previews a plan, floor, or building has a last input *Preview Location* that moves only its preview (D-062, [below](#grasshopper-components-through-s83)).
with
- All logic lives in `Lod.Core` and `Lod.Generators`, which reference neither Rhino nor Grasshopper; their tests run with `dotnet test`. `Lod.Grasshopper` wraps each pipeline type in a Goo with a viewport preview and exposes one component per step (D-017, D-018). Every component that previews a plan, floor, or building has a last input *Preview Location* that moves only its preview (D-062, [below](#grasshopper-components-through-s84)).
- Replace
| Types | `Schedule`, `LoadDefinition`, `Thermostat`, `ZoneProgram`, `ProgramPreset`, `PresetValues`, `PresetLoadValue`, `ExampleResidentialPresets` |
| Components | *Schedule*, *Load*, *Program Preset* (input *Conditioned*), *Dwelling Unit Preset*, *Corridor Preset*, *Stair Preset* |
| Decisions | D-024, D-026, D-038, D-047, D-061; ADR-003, ADR-005, ADR-007 |
with
| Types | `Schedule`, `LoadDefinition`, `Thermostat`, `ZoneProgram`, `ProgramPreset`, `PresetValues`, `PresetLoadValue`, `ExampleResidentialPresets`, `ExampleNonResidentialPresets` |
| Components | *Schedule*, *Load*, *Program Preset* (input *Conditioned*), *Dwelling Unit Preset*, *Corridor Preset*, *Stair Preset*, and the fifteen non-residential default preset components (*Office Preset* … *Service Preset*, S8.4) |
| Decisions | D-024, D-026, D-038, D-047, D-061, D-094, D-097; ADR-003, ADR-005, ADR-007, [ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md) |
- Replace
- `ExampleResidentialPresets` hold illustrative round numbers for development and tests, not DOE prototype values: dwelling unit and corridor conditioned, stair unconditioned. Each is available as values (`DwellingUnitValues`, `CorridorValues`, `StairValues`) and as the preset built from them (`DwellingUnit`, `Corridor`, `Stair`). The default preset components *Dwelling Unit Preset*, *Corridor Preset*, and *Stair Preset* take these values as their input defaults, so each works with nothing connected, and each shows the remark "Illustrative values; not sourced from DOE prototypes or standards."
with
- `ExampleResidentialPresets` hold illustrative round numbers for development and tests, not DOE prototype values: dwelling unit and corridor conditioned, stair unconditioned. Each is available as values (`DwellingUnitValues`, `CorridorValues`, `StairValues`) and as the preset built from them (`DwellingUnit`, `Corridor`, `Stair`). The default preset components *Dwelling Unit Preset*, *Corridor Preset*, and *Stair Preset* take these values as their input defaults, so each works with nothing connected, and each shows the remark "Illustrative values; not sourced from DOE prototypes or standards."
- `ExampleNonResidentialPresets` (S8.4, ADR-014) hold illustrative presets, all conditioned, for the space types of the non-residential departments (D-097): `Office`, `Core`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, `Lobby`, `ActivityHall`, and `Service`; the kitchen has gas equipment and, like the operating theatre, ventilation in air changes per hour; the retail schedules have their own Saturday and Sunday profiles. One default preset component per type, *Office Preset* to *Service Preset*, takes these values as input defaults with the same remark. Values and schedules: [program presets](../research/program-presets.md) §2.2.
- Replace
| Types | `PlanGenerator<TParameters, TPresets>` → `IGeneratedPlan`: `LinearPlanGenerator`, `StairBayBarGenerator`, `StairPairGenerator`, `GalleryBarGenerator`, `TerraceRowGenerator`, `PointPlateGenerator`, `WingedBandGenerator`, `OpenCourtGenerator`, `EnclosedCourtGenerator`, each with its `<Family>Parameters` and `<Family>Presets`; `GeometryLimits`; `PlanTransform.Apply(IGeneratedPlan, rotationDegrees, translationX, translationY)` → `IGeneratedPlan` |
| Components | *Linear Plan Generator*, *Stair Bay Bar*, *Stair Pair*, *Gallery Bar*, *Terrace Row*, *Point Plate*, *Winged Band*, *Open Court*, *Enclosed Court*, *Transform Plan* |
| Decisions | D-009, D-015, D-026, D-039, D-061, D-070, D-082, D-083, D-084; ADR-002, [ADR-006](../decisions/ADR-006-plan-generation-mechanism.md), [ADR-011](../decisions/ADR-011-minimum-wall-and-window-geometry.md), [ADR-013](../decisions/ADR-013-vertical-aggregation-methods.md) |
with
| Types | `PlanGenerator<TParameters, TPresets>` → `IGeneratedPlan`: `LinearPlanGenerator`, `StairBayBarGenerator`, `StairPairGenerator`, `GalleryBarGenerator`, `TerraceRowGenerator`, `PointPlateGenerator`, `WingedBandGenerator`, `OpenCourtGenerator`, `EnclosedCourtGenerator`, `OfficePlateGenerator`, `CafeteriaGenerator`, each with its `<Family>Parameters` and `<Family>Presets`; `GeometryLimits`; `PlanTransform.Apply(IGeneratedPlan, rotationDegrees, translationX, translationY)` → `IGeneratedPlan` |
| Components | *Linear Plan Generator*, *Stair Bay Bar*, *Stair Pair*, *Gallery Bar*, *Terrace Row*, *Point Plate*, *Winged Band*, *Open Court*, *Enclosed Court*, *Office Plate*, *Cafeteria*, *Transform Plan* |
| Decisions | D-009, D-015, D-026, D-039, D-061, D-070, D-082, D-083, D-084, D-093, D-094, D-097, D-098; ADR-002, [ADR-006](../decisions/ADR-006-plan-generation-mechanism.md), [ADR-011](../decisions/ADR-011-minimum-wall-and-window-geometry.md), [ADR-013](../decisions/ADR-013-vertical-aggregation-methods.md) |
- Replace
- Mechanism (ADR-006): one generator per typology family of the precedent study ([families](../research/precedents/hdhm-typologies.md), D-082, D-083), each a `PlanGenerator` subclass in `Lod.Generators/<Family>/` whose layout returns the footprint and the zones as polygons; the base checks the presets, builds the surfaces, places the windows, and records provenance. Layouts are axis-aligned in the plan frame (origin at the south-west corner, the bar along plan east); *Orientation* turns plan north and *Transform Plan* moves the result. Parameter records of S8 have a static `Default` with the ADR-006 defaults, which the components use as their input defaults. A parameter that would remove one of the family's essential relationships (a count below the family minimum) or a length that is not positive is the error `InvalidParameter`. Lengths are metres.
with
- Mechanism (ADR-006): one generator per typology family of the precedent study ([four-source families](../research/precedents/syn-typologies.md), D-093; the nine *High-Density Housing* families are carried as `SYN-TYP-001`–`009`, [hdhm-typologies.md](../research/precedents/hdhm-typologies.md), D-082, D-083), each a `PlanGenerator` subclass in `Lod.Generators/<Family>/` whose layout returns the footprint and the zones as polygons; the base checks the presets, builds the surfaces, places the windows, and records provenance. Layouts are axis-aligned in the plan frame (origin at the south-west corner, the bar along plan east); *Orientation* turns plan north and *Transform Plan* moves the result. Parameter records of S8 have a static `Default` with the ADR-006 defaults, which the components use as their input defaults. A parameter that would remove one of the family's essential relationships (a count below the family minimum) or a length that is not positive is the error `InvalidParameter`. Lengths are metres.
- Replace
- Enclosed court (`HDHM-TYP-005`): four wings of stair bays, `Depth` 12 m deep, around a court `CourtWidth` 24 m wide and `CourtDepth` 24 m deep that is closed on all four sides: south and north wings across the full width `2 · Depth + CourtWidth` that hold the corners, and west and east wings of length `CourtDepth` between them. The footprint is the outer rectangle with the court as its one hole, so the court is outside the building and its façades are outdoor walls with windows, facing into it. Bays are fitted to each wing as in the winged band (by default three bays of 6.5 m dwellings in each 48 m long wing and one bay of 10.5 m dwellings in each side wing). Zones `S-…`, `N-…` (west to east), then `W-…` and `E-…` (south to north); 1728 m², 44 windows, 16 of them on the court façades.
with
- Enclosed court (`HDHM-TYP-005`): four wings of stair bays, `Depth` 12 m deep, around a court `CourtWidth` 24 m wide and `CourtDepth` 24 m deep that is closed on all four sides: south and north wings across the full width `2 · Depth + CourtWidth` that hold the corners, and west and east wings of length `CourtDepth` between them. The footprint is the outer rectangle with the court as its one hole, so the court is outside the building and its façades are outdoor walls with windows, facing into it. Bays are fitted to each wing as in the winged band (by default three bays of 6.5 m dwellings in each 48 m long wing and one bay of 10.5 m dwellings in each side wing). Zones `S-…`, `N-…` (west to east), then `W-…` and `E-…` (south to north); 1728 m², 44 windows, 16 of them on the court façades.
- Office plate (`SYN-TYP-017`, S8.4): a `PlateLength` 42 m × `PlateDepth` 30 m plate with a central `CoreLength` 18 m × `CoreDepth` 6 m core `SC` (*Service Core*, a `Core` zone without outdoor walls) and one office work area `OF` (*Office*), the plate minus the core: a zone with the core as its hole in a footprint without one (D-097). The core must leave at least 3 m of work area at every façade (`OfficePlateGenerator.MinimumOfficeDepth`), or the error `InvalidParameter` names the depth left. Floor height 3.8 m; 1260 m², 4 windows.
- Cafeteria (`SYN-TYP-016`, S8.4): a `Length` 30 m plan with the kitchen and servery `KS` (`Kitchen`, `KitchenDepth` 8 m) along the whole north edge of the dining field `DI` (`Dining`, `DiningDepth` 15 m); the servery counter is the shared edge, not a zone. `KitchenDepth` must be smaller than `DiningDepth` (`InvalidParameter`). Floor height 4 m; 690 m², 6 windows.
- Non-residential families place one zone per department (D-097, [ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md)); their default dimensions are controller design defaults (D-098). The remaining families of the four-source set are specified in ADR-006 and built in S8.5–S8.7.
- Replace
## Grasshopper components through S8.3
with
## Grasshopper components through S8.4
- Replace
- **Preview Location (D-062).** The nine plan generators, *Transform Plan*, the four plan simplifiers, and the four floor aggregators have an optional last input *Preview Location* (PL): a vector in model units, default `0,0,0` (a point converts to a vector). It moves only that component's viewport preview: the plan, floor, or building data, *Inspect*, *Validate*, and *Convert2BEM* keep the true coordinates, and downstream components do not inherit the move (each wraps its output with its own input). *Convert2BEM* has no such input.
with
- **Preview Location (D-062).** The eleven plan generators, *Transform Plan*, the four plan simplifiers, and the four floor aggregators have an optional last input *Preview Location* (PL): a vector in model units, default `0,0,0` (a point converts to a vector). It moves only that component's viewport preview: the plan, floor, or building data, *Inspect*, *Validate*, and *Convert2BEM* keep the true coordinates, and downstream components do not inherit the move (each wraps its output with its own input). *Convert2BEM* has no such input.
- Replace
- **Typology generators (S8.1, ADR-006).** *Stair Bay Bar*, *Stair Pair*, *Gallery Bar*, and *Terrace Row* follow *Linear Plan Generator*: their number inputs (counts are integers) default to the family's `Default` record, then *Floor Height* (3 m) and *Orientation* (0°), then one required preset input per space type they place (*Dwelling Unit*, and *Stair* except for *Terrace Row*; an unconnected one is Grasshopper's missing-input warning, D-065), then *Preview Location*. Their descriptions name the family and what is not modelled. `WindowOmitted` shows as a remark. *Linear Plan Generator*'s description names `HDHM-TYP-009`; its inputs and GUID are unchanged. *Point Plate*, *Winged Band*, and *Open Court* (S8.2) follow the same pattern with the preset inputs *Dwelling Unit* and *Stair*; on *Point Plate* the *Stair* preset is the core's. *Enclosed Court* (S8.3) follows the same pattern; its plan previews the court as a hole, and every zone, floor, and ceiling Brep of a footprint with holes is one face or closed extrusion with an inner loop per hole (`RhinoGeometry`).
with
- **Typology generators (S8.1, ADR-006).** *Stair Bay Bar*, *Stair Pair*, *Gallery Bar*, and *Terrace Row* follow *Linear Plan Generator*: their number inputs (counts are integers) default to the family's `Default` record, then *Floor Height* (3 m) and *Orientation* (0°), then one required preset input per space type they place (*Dwelling Unit*, and *Stair* except for *Terrace Row*; an unconnected one is Grasshopper's missing-input warning, D-065), then *Preview Location*. Their descriptions name the family and what is not modelled. `WindowOmitted` shows as a remark. *Linear Plan Generator*'s description names `HDHM-TYP-009`; its inputs and GUID are unchanged. *Point Plate*, *Winged Band*, and *Open Court* (S8.2) follow the same pattern with the preset inputs *Dwelling Unit* and *Stair*; on *Point Plate* the *Stair* preset is the core's. *Enclosed Court* (S8.3) follows the same pattern; its plan previews the court as a hole, and every zone, floor, and ceiling Brep of a footprint with holes is one face or closed extrusion with an inner loop per hole (`RhinoGeometry`). *Office Plate* and *Cafeteria* (S8.4) follow the same pattern with the preset inputs *Office* and *Core*, and *Kitchen* and *Dining*; their descriptions name the `SYN-TYP-*` family. The office work area is previewed as a ring around the core.
- **Default preset components of the non-residential types (S8.4, ADR-014).** *Office Preset* to *Service Preset* share the inputs of the residential ones (above), one value and one optional schedule input per load of their example preset; *Gas Equipment* is in W/m², *Ventilation* and *Infiltration* in 1/h. Zones of the new types have their own preview colours.
- Replace
| 1 Program | *Stair Preset* (Stair) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `StairPresetComponent` | `717ccc25-1097-4b66-addc-d2fcdd4e385a` | S4.1 |
with
| 1 Program | *Stair Preset* (Stair) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `StairPresetComponent` | `717ccc25-1097-4b66-addc-d2fcdd4e385a` | S4.1 |
| 1 Program | *Office Preset* (Office) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `OfficePresetComponent` | `4160f791-07dc-4529-9986-1c9ba7d21a98` | S8.4 |
| 1 Program | *Core Preset* (Core) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `CorePresetComponent` | `ca44b061-1ced-4ae3-afed-d806e6db2b7c` | S8.4 |
| 1 Program | *Retail Preset* (Retail) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `RetailPresetComponent` | `b760860d-05c9-4cbe-a22f-ddd2e38e4d64` | S8.4 |
| 1 Program | *Mall Preset* (Mall) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `MallPresetComponent` | `dbd514d5-7553-4000-b10c-bca748e3b301` | S8.4 |
| 1 Program | *Kitchen Preset* (Kitch) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Gas Equipment, Gas Equipment Schedule, Ventilation, Ventilation Schedule, Infiltration, Infiltration Schedule → Preset | `KitchenPresetComponent` | `658e05dc-c935-4518-bc99-512546153e1e` | S8.4 |
| 1 Program | *Dining Preset* (Dine) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `DiningPresetComponent` | `c8b05fa2-330a-4e8a-acaa-e57ca57b4fd9` | S8.4 |
| 1 Program | *Operating Theatre Preset* (OT) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Ventilation, Ventilation Schedule, Infiltration, Infiltration Schedule → Preset | `OperatingTheatrePresetComponent` | `eb55b6d5-08f1-4a5e-9598-13951c34c2a4` | S8.4 |
| 1 Program | *Clean Corridor Preset* (Clean) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `CleanCorridorPresetComponent` | `c199a986-28b8-41c7-b260-5f4581b7362b` | S8.4 |
| 1 Program | *Dirty Corridor Preset* (Dirty) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `DirtyCorridorPresetComponent` | `0e61c86e-5981-4a11-b922-52650757e4a9` | S8.4 |
| 1 Program | *Clinical Support Preset* (ClinS) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `ClinicalSupportPresetComponent` | `274e0ac9-de95-4bd4-8028-25d74bfd1e4b` | S8.4 |
| 1 Program | *Care Bedroom Preset* (CBed) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `CareBedroomPresetComponent` | `8447ea38-faec-4630-96dd-92569cd7d0d8` | S8.4 |
| 1 Program | *Care Communal Preset* (CComm) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `CareCommunalPresetComponent` | `a3000f8c-633f-4a54-9fb8-1a4109e337bb` | S8.4 |
| 1 Program | *Lobby Preset* (Lobby) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `LobbyPresetComponent` | `395aebb3-3e49-4814-93ab-388da9de2360` | S8.4 |
| 1 Program | *Activity Hall Preset* (Hall) | Name, WWR, Conditioned, Heating, Cooling, Occupancy, Occupancy Schedule, Lighting, Lighting Schedule, Equipment, Equipment Schedule, Infiltration, Infiltration Schedule → Preset | `ActivityHallPresetComponent` | `6bf00cd3-70b9-4337-8b80-70fc00d79666` | S8.4 |
| 1 Program | *Service Preset* (Serv) | Name, WWR, Conditioned, Heating, Cooling, Lighting, Lighting Schedule, Infiltration, Infiltration Schedule → Preset | `ServicePresetComponent` | `60251c8e-ad4b-4ef5-8f68-64f4c6bf6f8b` | S8.4 |
- Replace
| 2 Generate | *Enclosed Court* (ECourt) | Court Width (24), Court Depth (24), Depth (12), Unit Width (7), Stair Width (3), Floor Height (3), Orientation (0), Dwelling Unit, Stair, Preview Location → Plan | `EnclosedCourtComponent` | `b9765cd0-a62b-483d-9470-86d5fda287e8` | S8.3 |
with
| 2 Generate | *Enclosed Court* (ECourt) | Court Width (24), Court Depth (24), Depth (12), Unit Width (7), Stair Width (3), Floor Height (3), Orientation (0), Dwelling Unit, Stair, Preview Location → Plan | `EnclosedCourtComponent` | `b9765cd0-a62b-483d-9470-86d5fda287e8` | S8.3 |
| 2 Generate | *Office Plate* (OPlate) | Plate Length (42), Plate Depth (30), Core Length (18), Core Depth (6), Floor Height (3.8), Orientation (0), Office, Core, Preview Location → Plan | `OfficePlateComponent` | `783cefaa-b836-4dbc-b16d-8e74820ed361` | S8.4 |
| 2 Generate | *Cafeteria* (Cafe) | Length (30), Dining Depth (15), Kitchen Depth (8), Floor Height (4), Orientation (0), Kitchen, Dining, Preview Location → Plan | `CafeteriaComponent` | `95956724-ddbc-48ce-9874-0ec6c244c8fe` | S8.4 |
- Replace
| Item | Stage | Gate |
| --- | --- | --- |
with
| Item | Stage | Gate |
| --- | --- | --- |
| Plan generators for the four-source families `SYN-TYP-010`, `014`, `018`, `019` (S8.5), `015`, `011` (S8.6), and `012` with plans per storey (S8.7), specified in ADR-006 | S8.5–S8.7 | an ADR on plans per storey (S8.7, D-095) |
- Replace
- [ADR-013 Vertical aggregation methods](../decisions/ADR-013-vertical-aggregation-methods.md)
with
- [ADR-013 Vertical aggregation methods](../decisions/ADR-013-vertical-aggregation-methods.md)
- [ADR-014 Program types and department zoning](../decisions/ADR-014-program-types-and-department-zoning.md)
- [Four-source typology families](../research/precedents/syn-typologies.md)
- [x] Step 3: Edit
docs/architecture/convert2bem.md
Make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
| 2 | Space Types | ST | `{i}`: one text | `SpaceType` name: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, `Other`, or `Mixed`. | — |
with
| 2 | Space Types | ST | `{i}`: one text | `SpaceType` name: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, `Other`, `Mixed`, or one of the non-residential types of ADR-014 (`Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, `ActivityHall`). | — |
- [x] Step 4: Edit
docs/architecture/domain-model.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
**`SpaceType`** is the semantic category of a space: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, `Other`, and `Mixed` for a simplified zone that combines several space types. Semantics are explicit data, never encoded in layer names, colours, or wire structure (AGENTS.md).
with
**`SpaceType`** is the semantic category of a space: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, `Other`, `Mixed` for a simplified zone that combines several space types, and since S8.4 the non-residential departments `Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, and `ActivityHall`, appended so that existing values keep their order ([ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md)). A non-residential zone is one department, never a room (D-097). Semantics are explicit data, never encoded in layer names, colours, or wire structure (AGENTS.md).
- Replace
**`ExampleResidentialPresets`** provides illustrative presets for dwelling unit, corridor, and stair; the dwelling unit and corridor are conditioned and the stair is not. Each is available as `PresetValues` (`DwellingUnitValues`, `CorridorValues`, `StairValues`) and as the `ProgramPreset` built from them (`DwellingUnit`, `Corridor`, `Stair`). Their values and conditioning are round-number choices for development and tests, not DOE prototype values; see [program presets](../research/program-presets.md).
with
**`ExampleResidentialPresets`** provides illustrative presets for dwelling unit, corridor, and stair; the dwelling unit and corridor are conditioned and the stair is not. Each is available as `PresetValues` (`DwellingUnitValues`, `CorridorValues`, `StairValues`) and as the `ProgramPreset` built from them (`DwellingUnit`, `Corridor`, `Stair`). Their values and conditioning are round-number choices for development and tests, not DOE prototype values; see [program presets](../research/program-presets.md).
**`ExampleNonResidentialPresets`** (S8.4) provides illustrative presets, all conditioned, for the fifteen space types of the non-residential departments (ADR-014): `OfficeValues`/`Office`, `CoreValues`/`Core`, … `ServiceValues`/`Service`; `SpaceTypes` lists the types and `ValuesOf` and `PresetOf` look a type up (`ArgumentOutOfRangeException` for any other type). The retail schedules have their own Saturday and Sunday profiles. The values are illustrative, not sourced (D-023).
- [x] Step 5: Edit
docs/decisions/ADR-002-geometry.md
Make this replacement; the "Replace" text occurs exactly once in the file.
- Replace
**Footprints with holes (S8.3).** A court is a hole of the footprint, outside the building: its edges are outdoor walls with windows by the generator's rule, it has no zone, and no floor or ceiling covers it, so it adds neither ground nor roof area. Every plan simplifier and floor aggregator supports such footprints ([ADR-008](ADR-008-perimeter-core-corners.md) for *Perimeter Core*, [ADR-013](ADR-013-vertical-aggregation-methods.md) for the floor aggregators). In Grasshopper a polygon with holes is one planar face with an inner loop per hole, and a zone part one closed extrusion of that face.
with
**Footprints with holes (S8.3).** A court is a hole of the footprint, outside the building: its edges are outdoor walls with windows by the generator's rule, it has no zone, and no floor or ceiling covers it, so it adds neither ground nor roof area. Every plan simplifier and floor aggregator supports such footprints ([ADR-008](ADR-008-perimeter-core-corners.md) for *Perimeter Core*, [ADR-013](ADR-013-vertical-aggregation-methods.md) for the floor aggregators). In Grasshopper a polygon with holes is one planar face with an inner loop per hole, and a zone part one closed extrusion of that face. A zone may also have a hole inside a footprint without one: the office work area of `SYN-TYP-017` (S8.4, [ADR-006](ADR-006-plan-generation-mechanism.md)) is a ring around the core; the hole's edges are interzone walls with the zone inside it, and no step needed a change for it.
- [x] Step 6: Edit
docs/decisions/ADR-006-plan-generation-mechanism.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
- Not modelled: individual work areas, meeting rooms, and the rooms of the core (D-097); a core off the centre; non-rectangular plates. *Perimeter Core* still needs every part of the plate wider than twice its depth (D-087); the default plate passes with 4.57 m.
with
- Not modelled: individual work areas, meeting rooms, and the rooms of the core (D-097); a core off the centre; non-rectangular plates. *Perimeter Core* still needs every part of the plate wider than twice its depth (D-087); the default plate passes with 4.57 m.
- Implemented (S8.4): as specified. Zones `SC` (*Service Core*), `OF` (*Office*); by default 108 m² and 1152 m², 4 windows of WWR 0.4 (218.88 m² of glazing), none on the core, whose four walls are all interzone walls with the office. The office's hole is the core's ring, so the office has eight walls: four outdoor and four facing the core. *Semantic Merge* keeps the two zones (the ring keeps its hole); *Perimeter Core* gives `P-North`, `P-East`, `P-South`, `P-West`, and a `CORE` of 32.86 m × 20.86 m (685.4596 m²) that contains the whole service core and the inner part of the ring; the single-zone methods drop the ring.
- Replace
- Not modelled: the counter, the queue, the kitchen's rooms (stores, wash-up), seating, and entrances (D-097); a service edge on more than one side.
with
- Not modelled: the counter, the queue, the kitchen's rooms (stores, wash-up), seating, and entrances (D-097); a service edge on more than one side.
- Implemented (S8.4): as specified. Zones `KS` (*Kitchen and Servery*, 240 m²) and `DI` (*Dining*, 450 m²), sharing one 30 m interzone wall; 6 windows (114.4 m² of glazing), three per zone. *Perimeter Core* puts the kitchen alone in `P-North`, the dining field alone in `P-South`, and both in `P-East`, `P-West`, and `CORE`.
#### Pipeline consequences of S8.4
The office plate's work area is the first generated zone with a hole in a footprint without one. The surface builder pairs the hole's edges with the core's edges as interzone walls, and every plan simplifier and floor aggregator handles the ring as it handles the united rings of *Semantic Merge* (S8.2): every combination validates for both families, canonical and at the two rotated placements of the test suite (and, in a sweep run while building the reference, at 50 rotations from 0° to 357.7° for the default plans and for an office plate with the 3 m minimum depth). No step of `Lod.Core` changed, and no tolerance or check was relaxed.
- [x] Step 7: Edit
docs/research/program-presets.md
Make these replacements, in order; each "Replace" text occurs exactly once in the file when it is applied.
- Replace
> **Status:** Current as of S4.1 (`v0.4.1`) · **Date:** 2026-10-01 · **Decisions:** D-009, D-024, D-026, D-038, D-039, D-047, D-061, D-079
with
> **Status:** Current as of S8.4 (`v0.8.3`) · **Date:** 2026-10-02 · **Decisions:** D-009, D-024, D-026, D-038, D-039, D-047, D-061, D-079, D-094, D-097; ADR-014
- Replace
> **Read this first:** the presets that ship with BEMGen (`ExampleResidentialPresets`) hold **illustrative round numbers chosen for development and tests, and which of them are conditioned is an illustrative choice too. They are NOT taken from the DOE prototype buildings or from any standard.** Sourced presets are an open research input (§3).
with
> **Read this first:** the presets that ship with BEMGen (`ExampleResidentialPresets` and, since S8.4, `ExampleNonResidentialPresets`) hold **illustrative round numbers chosen for development and tests, and which of them are conditioned is an illustrative choice too. They are NOT taken from the DOE prototype buildings or from any standard.** Sourced presets are an open research input (§3).
- Replace
A program preset carries everything BEMGen assigns to a space of one type: its loads with their schedules, whether it is conditioned and, if so, its heating and cooling setpoints, and its window parameter (D-024, D-038). There is one preset per space type. Rooms are never modelled: at the most detailed level (Z0) a dwelling unit is one zone (D-009).
with
A program preset carries everything BEMGen assigns to a space of one type: its loads with their schedules, whether it is conditioned and, if so, its heating and cooling setpoints, and its window parameter (D-024, D-038). There is one preset per space type. Rooms are never modelled: at the most detailed level (Z0) a dwelling unit is one zone per storey (D-009, D-085), and a non-residential floor has one zone per department, a contiguous group of rooms of one program (D-097, ADR-014).
- Replace
| `SpaceType` | The space type the preset applies to: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, or `Other`. (`Mixed` denotes a simplified zone that combines several space types.) | — |
with
| `SpaceType` | The space type the preset applies to: `DwellingUnit`, `Corridor`, `Stair`, `Core`, `Lobby`, `Service`, `Mechanical`, `Other`, and since S8.4 the non-residential types `Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, and `ActivityHall` (ADR-014). (`Mixed` denotes a simplified zone that combines several space types.) | — |
- Replace
> **The values below, and the choice of which spaces are conditioned, are illustrative and chosen for development and tests. They are NOT taken from the DOE prototype buildings, from any standard, or from measured data. Do not use them as research inputs, and do not present results obtained with them as representative of any prototype or real building.**
with
> **The values below, and the choice of which spaces are conditioned, are illustrative and chosen for development and tests. They are NOT taken from the DOE prototype buildings, from any standard, or from measured data. Do not use them as research inputs, and do not present results obtained with them as representative of any prototype or real building.**
### 2.1 Residential presets
- Replace
| 23 | 1.0 | 1.0 | 0.2 | 0.2 |
## 3. Sourced presets: an open research input
with
| 23 | 1.0 | 1.0 | 0.2 | 0.2 |
### 2.2 Non-residential presets (S8.4)
> **The values below are illustrative, like those of §2.1: plausible round numbers for development and tests, NOT taken from the DOE prototype buildings, from any standard, or from measured data.** Their reasons are recorded in [ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md).
[`ExampleNonResidentialPresets`](../../src/Lod.Core/Programs/ExampleNonResidentialPresets.cs) defines one preset for each space type that the non-residential families place (D-094, D-097): the twelve new types and `Core`, `Lobby`, and `Service`. Each is available as `PresetValues` (`OfficeValues`, …, `ServiceValues`) and as the preset built from them (`Office`, …, `Service`); `SpaceTypes`, `ValuesOf(SpaceType)`, and `PresetOf(SpaceType)` list and look them up. Every one is conditioned. The Grasshopper components *Office Preset* to *Service Preset* use these values as the defaults of their inputs (§5).
| Preset (`Name`) | Space type | Heating | Cooling | WWR |
| --- | --- | --- | --- | --- |
| `Example Office` | `Office` | 21 °C | 24 °C | 0.4 |
| `Example Core` | `Core` | 21 °C | 24 °C | 0 |
| `Example Retail` | `Retail` | 20 °C | 24 °C | 0.3 |
| `Example Mall` | `Mall` | 20 °C | 24 °C | 0.2 |
| `Example Kitchen` | `Kitchen` | 18 °C | 26 °C | 0.1 |
| `Example Dining` | `Dining` | 21 °C | 24 °C | 0.4 |
| `Example Operating Theatre` | `OperatingTheatre` | 20 °C | 22 °C | 0 |
| `Example Clean Corridor` | `CleanCorridor` | 20 °C | 24 °C | 0.1 |
| `Example Dirty Corridor` | `DirtyCorridor` | 20 °C | 24 °C | 0.1 |
| `Example Clinical Support` | `ClinicalSupport` | 21 °C | 24 °C | 0.2 |
| `Example Care Bedroom` | `CareBedroom` | 22 °C | 25 °C | 0.3 |
| `Example Care Communal` | `CareCommunal` | 22 °C | 25 °C | 0.4 |
| `Example Lobby` | `Lobby` | 20 °C | 24 °C | 0.5 |
| `Example Activity Hall` | `ActivityHall` | 18 °C | 25 °C | 0.2 |
| `Example Service` | `Service` | 18 °C | 26 °C | 0.1 |
| Preset | Load type | Basis | Value | Schedule (annual sum of fractions) |
| --- | --- | --- | --- | --- |
| Office | `Occupancy` | `PerFloorArea` | 0.1 people/m² | `Example Office Occupancy` (2231.55) |
| Office | `Lighting` | `PerFloorArea` | 10 W/m² | `Example Office Lighting` (2695.65) |
| Office | `ElectricEquipment` | `PerFloorArea` | 10 W/m² | `Example Office Equipment` (3526.8) |
| Office | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Core | `Lighting` | `PerFloorArea` | 5 W/m² | `Example Office Lighting` (2695.65) |
| Core | `ElectricEquipment` | `PerFloorArea` | 3 W/m² | `Example Office Equipment` (3526.8) |
| Core | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Retail | `Occupancy` | `PerFloorArea` | 0.2 people/m² | `Example Retail Occupancy` (2414.4) |
| Retail | `Lighting` | `PerFloorArea` | 15 W/m² | `Example Retail Lighting` (4547.5) |
| Retail | `ElectricEquipment` | `PerFloorArea` | 5 W/m² | `Example Retail Equipment` (5100) |
| Retail | `Infiltration` | `AirChangesPerHour` | 0.5 1/h | `Example Always On` (8760) |
| Mall | `Occupancy` | `PerFloorArea` | 0.1 people/m² | `Example Retail Occupancy` (2414.4) |
| Mall | `Lighting` | `PerFloorArea` | 10 W/m² | `Example Retail Lighting` (4547.5) |
| Mall | `Infiltration` | `AirChangesPerHour` | 0.8 1/h | `Example Always On` (8760) |
| Kitchen | `Occupancy` | `PerFloorArea` | 0.05 people/m² | `Example Kitchen Staff` (2244.6) |
| Kitchen | `Lighting` | `PerFloorArea` | 12 W/m² | `Example Kitchen Hours` (2610) |
| Kitchen | `ElectricEquipment` | `PerFloorArea` | 30 W/m² | `Example Kitchen Electric` (2952.6) |
| Kitchen | `GasEquipment` | `PerFloorArea` | 40 W/m² | `Example Kitchen Gas` (1122.3) |
| Kitchen | `Ventilation` | `AirChangesPerHour` | 15 1/h | `Example Kitchen Hours` (2610) |
| Kitchen | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Dining | `Occupancy` | `PerFloorArea` | 0.6 people/m² | `Example Dining Occupancy` (809.1) |
| Dining | `Lighting` | `PerFloorArea` | 10 W/m² | `Example Dining Hours` (2349) |
| Dining | `ElectricEquipment` | `PerFloorArea` | 2 W/m² | `Example Dining Hours` (2349) |
| Dining | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Operating Theatre | `Occupancy` | `PerFloorArea` | 0.1 people/m² | `Example Theatre Sessions` (2871) |
| Operating Theatre | `Lighting` | `PerFloorArea` | 25 W/m² | `Example Theatre Services` (4048.8) |
| Operating Theatre | `ElectricEquipment` | `PerFloorArea` | 30 W/m² | `Example Theatre Services` (4048.8) |
| Operating Theatre | `Ventilation` | `AirChangesPerHour` | 20 1/h | `Example Theatre Ventilation` (5946) |
| Operating Theatre | `Infiltration` | `AirChangesPerHour` | 0.1 1/h | `Example Always On` (8760) |
| Clean Corridor | `Lighting` | `PerFloorArea` | 8 W/m² | `Example Always On` (8760) |
| Clean Corridor | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Dirty Corridor | `Lighting` | `PerFloorArea` | 8 W/m² | `Example Always On` (8760) |
| Dirty Corridor | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Clinical Support | `Occupancy` | `PerFloorArea` | 0.05 people/m² | `Example Theatre Sessions` (2871) |
| Clinical Support | `Lighting` | `PerFloorArea` | 12 W/m² | `Example Theatre Services` (4048.8) |
| Clinical Support | `ElectricEquipment` | `PerFloorArea` | 10 W/m² | `Example Theatre Services` (4048.8) |
| Clinical Support | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Care Bedroom | `Occupancy` | `PerFloorArea` | 0.04 people/m² | `Example Care Bedroom Occupancy` (5803.5) |
| Care Bedroom | `Lighting` | `PerFloorArea` | 6 W/m² | `Example Care Bedroom Lighting` (2226.5) |
| Care Bedroom | `ElectricEquipment` | `PerFloorArea` | 3 W/m² | `Example Care Bedroom Equipment` (4380) |
| Care Bedroom | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Care Communal | `Occupancy` | `PerFloorArea` | 0.15 people/m² | `Example Care Communal Occupancy` (3777.75) |
| Care Communal | `Lighting` | `PerFloorArea` | 10 W/m² | `Example Care Communal Hours` (6132) |
| Care Communal | `ElectricEquipment` | `PerFloorArea` | 5 W/m² | `Example Care Communal Hours` (6132) |
| Care Communal | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Lobby | `Occupancy` | `PerFloorArea` | 0.1 people/m² | `Example Community Occupancy` (2444.5) |
| Lobby | `Lighting` | `PerFloorArea` | 10 W/m² | `Example Community Hours` (5267) |
| Lobby | `Infiltration` | `AirChangesPerHour` | 0.6 1/h | `Example Always On` (8760) |
| Activity Hall | `Occupancy` | `PerFloorArea` | 0.1 people/m² | `Example Community Occupancy` (2444.5) |
| Activity Hall | `Lighting` | `PerFloorArea` | 12 W/m² | `Example Community Hours` (5267) |
| Activity Hall | `ElectricEquipment` | `PerFloorArea` | 2 W/m² | `Example Community Hours` (5267) |
| Activity Hall | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
| Service | `Lighting` | `PerFloorArea` | 6 W/m² | `Example Community Hours` (5267) |
| Service | `Infiltration` | `AirChangesPerHour` | 0.3 1/h | `Example Always On` (8760) |
Ventilation is given only for the kitchen (hood extract) and the operating theatre; no preset defines `DomesticHotWater`. The year starts on a Monday. Monday to Friday use the weekday profile and Saturday and Sunday the weekend profile, except the three retail schedules, which have their own Saturday and Sunday profiles. Hourly fractions (hour 0 is the first hour of the day):
**Office and core.**
| Hour of day | Office Occupancy, weekday | Office Lighting, weekday | Office Equipment, weekday |
| --- | --- | --- | --- |
| 0 | 0 | 0.05 | 0.2 |
| 1 | 0 | 0.05 | 0.2 |
| 2 | 0 | 0.05 | 0.2 |
| 3 | 0 | 0.05 | 0.2 |
| 4 | 0 | 0.05 | 0.2 |
| 5 | 0 | 0.05 | 0.2 |
| 6 | 0 | 0.05 | 0.2 |
| 7 | 0.1 | 0.3 | 0.4 |
| 8 | 0.5 | 0.8 | 0.7 |
| 9 | 0.95 | 0.9 | 0.9 |
| 10 | 0.95 | 0.9 | 0.9 |
| 11 | 0.95 | 0.9 | 0.9 |
| 12 | 0.5 | 0.8 | 0.7 |
| 13 | 0.95 | 0.9 | 0.9 |
| 14 | 0.95 | 0.9 | 0.9 |
| 15 | 0.95 | 0.9 | 0.9 |
| 16 | 0.95 | 0.9 | 0.9 |
| 17 | 0.5 | 0.6 | 0.6 |
| 18 | 0.2 | 0.3 | 0.4 |
| 19 | 0.1 | 0.2 | 0.3 |
| 20 | 0 | 0.05 | 0.2 |
| 21 | 0 | 0.05 | 0.2 |
| 22 | 0 | 0.05 | 0.2 |
| 23 | 0 | 0.05 | 0.2 |
Weekend: occupancy 0, lighting 0.05, equipment 0.2 all day.
**Retail and mall** (Saturday and Sunday differ).
| Hour of day | Retail Occupancy, weekday | Saturday | Sunday | Retail Lighting, weekday and Saturday | Sunday | Retail Equipment, weekday and Saturday | Sunday |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 1 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 2 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 3 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 4 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 5 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 6 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 7 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 8 | 0 | 0 | 0 | 0.5 | 0.1 | 0.6 | 0.3 |
| 9 | 0.3 | 0.5 | 0 | 1 | 0.1 | 0.9 | 0.3 |
| 10 | 0.5 | 0.8 | 0 | 1 | 0.5 | 0.9 | 0.6 |
| 11 | 0.6 | 1 | 0.4 | 1 | 1 | 0.9 | 0.9 |
| 12 | 0.8 | 1 | 0.6 | 1 | 1 | 0.9 | 0.9 |
| 13 | 0.8 | 1 | 0.7 | 1 | 1 | 0.9 | 0.9 |
| 14 | 0.6 | 1 | 0.7 | 1 | 1 | 0.9 | 0.9 |
| 15 | 0.6 | 1 | 0.6 | 1 | 1 | 0.9 | 0.9 |
| 16 | 0.7 | 0.9 | 0.4 | 1 | 1 | 0.9 | 0.9 |
| 17 | 0.8 | 0.8 | 0 | 1 | 0.5 | 0.9 | 0.6 |
| 18 | 0.7 | 0.6 | 0 | 1 | 0.1 | 0.9 | 0.3 |
| 19 | 0.4 | 0.3 | 0 | 1 | 0.1 | 0.9 | 0.3 |
| 20 | 0 | 0 | 0 | 0.5 | 0.1 | 0.6 | 0.3 |
| 21 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 22 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
| 23 | 0 | 0 | 0 | 0.1 | 0.1 | 0.3 | 0.3 |
**Kitchen and dining** (weekday; weekend: kitchen electric 0.2, every other schedule 0).
| Hour of day | Kitchen Staff | Kitchen Hours | Kitchen Electric | Kitchen Gas | Dining Occupancy | Dining Hours |
| --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 4 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 5 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 6 | 0.3 | 1 | 0.4 | 0 | 0 | 0 |
| 7 | 0.8 | 1 | 0.6 | 0.3 | 0.1 | 1 |
| 8 | 1 | 1 | 0.6 | 0.3 | 0.2 | 1 |
| 9 | 1 | 1 | 0.6 | 0.3 | 0.1 | 1 |
| 10 | 1 | 1 | 0.6 | 0.6 | 0.1 | 1 |
| 11 | 1 | 1 | 0.9 | 1 | 0.4 | 1 |
| 12 | 1 | 1 | 1 | 1 | 1 | 1 |
| 13 | 1 | 1 | 0.9 | 0.6 | 0.8 | 1 |
| 14 | 1 | 1 | 0.6 | 0.2 | 0.3 | 1 |
| 15 | 0.5 | 1 | 0.4 | 0 | 0.1 | 1 |
| 16 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 17 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 18 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 19 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 20 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 21 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 22 | 0 | 0 | 0.2 | 0 | 0 | 0 |
| 23 | 0 | 0 | 0.2 | 0 | 0 | 0 |
**Operating suite** (weekday; weekend: sessions 0, services 0.2, ventilation 0.5).
| Hour of day | Theatre Sessions | Theatre Services | Theatre Ventilation |
| --- | --- | --- | --- |
| 0 | 0 | 0.2 | 0.5 |
| 1 | 0 | 0.2 | 0.5 |
| 2 | 0 | 0.2 | 0.5 |
| 3 | 0 | 0.2 | 0.5 |
| 4 | 0 | 0.2 | 0.5 |
| 5 | 0 | 0.2 | 0.5 |
| 6 | 0 | 0.2 | 0.5 |
| 7 | 0.5 | 0.6 | 1 |
| 8 | 1 | 1 | 1 |
| 9 | 1 | 1 | 1 |
| 10 | 1 | 1 | 1 |
| 11 | 1 | 1 | 1 |
| 12 | 1 | 1 | 1 |
| 13 | 1 | 1 | 1 |
| 14 | 1 | 1 | 1 |
| 15 | 1 | 1 | 1 |
| 16 | 1 | 1 | 1 |
| 17 | 1 | 1 | 1 |
| 18 | 0.5 | 0.6 | 1 |
| 19 | 0 | 0.2 | 0.5 |
| 20 | 0 | 0.2 | 0.5 |
| 21 | 0 | 0.2 | 0.5 |
| 22 | 0 | 0.2 | 0.5 |
| 23 | 0 | 0.2 | 0.5 |
**Elder care** (every day the same; `Example Care Bedroom Equipment` is constant 0.5).
| Hour of day | Care Bedroom Occupancy | Care Bedroom Lighting | Care Communal Occupancy | Care Communal Hours |
| --- | --- | --- | --- | --- |
| 0 | 1 | 0.05 | 0.05 | 0.2 |
| 1 | 1 | 0.05 | 0.05 | 0.2 |
| 2 | 1 | 0.05 | 0.05 | 0.2 |
| 3 | 1 | 0.05 | 0.05 | 0.2 |
| 4 | 1 | 0.05 | 0.05 | 0.2 |
| 5 | 1 | 0.05 | 0.05 | 0.2 |
| 6 | 1 | 0.3 | 0.05 | 0.2 |
| 7 | 0.8 | 0.6 | 0.3 | 1 |
| 8 | 0.5 | 0.4 | 0.9 | 1 |
| 9 | 0.3 | 0.1 | 0.6 | 1 |
| 10 | 0.3 | 0.1 | 0.7 | 1 |
| 11 | 0.3 | 0.1 | 0.7 | 1 |
| 12 | 0.2 | 0.1 | 1 | 1 |
| 13 | 0.4 | 0.1 | 0.6 | 1 |
| 14 | 0.4 | 0.1 | 0.7 | 1 |
| 15 | 0.4 | 0.1 | 0.7 | 1 |
| 16 | 0.3 | 0.2 | 0.7 | 1 |
| 17 | 0.2 | 0.4 | 1 | 1 |
| 18 | 0.4 | 0.6 | 0.8 | 1 |
| 19 | 0.6 | 0.8 | 0.6 | 1 |
| 20 | 0.8 | 0.8 | 0.4 | 1 |
| 21 | 1 | 0.6 | 0.2 | 1 |
| 22 | 1 | 0.3 | 0.05 | 0.2 |
| 23 | 1 | 0.1 | 0.05 | 0.2 |
**Community: foyer, activity hall, support rooms.**
| Hour of day | Community Occupancy, weekday | weekend | Community Hours, weekday | weekend |
| --- | --- | --- | --- | --- |
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 |
| 4 | 0 | 0 | 0 | 0 |
| 5 | 0 | 0 | 0 | 0 |
| 6 | 0 | 0 | 0 | 0 |
| 7 | 0 | 0 | 0 | 0 |
| 8 | 0 | 0 | 1 | 1 |
| 9 | 0.2 | 0.8 | 1 | 1 |
| 10 | 0.2 | 0.8 | 1 | 1 |
| 11 | 0.2 | 0.8 | 1 | 1 |
| 12 | 0.2 | 0.8 | 1 | 1 |
| 13 | 0.2 | 0.8 | 1 | 1 |
| 14 | 0.2 | 0.8 | 1 | 1 |
| 15 | 0.2 | 0.8 | 1 | 1 |
| 16 | 0.2 | 0.8 | 1 | 1 |
| 17 | 0.6 | 0.8 | 1 | 1 |
| 18 | 0.9 | 0.8 | 1 | 1 |
| 19 | 0.9 | 0.8 | 1 | 1 |
| 20 | 0.9 | 0.4 | 1 | 1 |
| 21 | 0.6 | 0 | 1 | 0 |
| 22 | 0.2 | 0 | 1 | 0 |
| 23 | 0 | 0 | 0 | 0 |
## 3. Sourced presets: an open research input
- Replace
S4.1 adds one default preset component per space type of the linear plan, *Dwelling Unit Preset*, *Corridor Preset*, and *Stair Preset*, and deletes *Example Residential Presets* (D-061). Every input of a default preset component has a default, the values of §2, so the component works with nothing connected: Name, WWR, Conditioned, Heating (°C), Cooling (°C), and per load a value input (people/m², W/m², or 1/h, as in its description) and an optional schedule input that replaces the built-in schedule when connected. Each shows the remark "Illustrative values; not sourced from DOE prototypes or standards." Their inputs and outputs are listed in [pipeline.md](../architecture/pipeline.md#grasshopper-components-through-s83).
with
S4.1 adds one default preset component per space type of the linear plan, *Dwelling Unit Preset*, *Corridor Preset*, and *Stair Preset*, and deletes *Example Residential Presets* (D-061). Every input of a default preset component has a default, the values of §2, so the component works with nothing connected: Name, WWR, Conditioned, Heating (°C), Cooling (°C), and per load a value input (people/m², W/m², or 1/h, as in its description) and an optional schedule input that replaces the built-in schedule when connected. Each shows the remark "Illustrative values; not sourced from DOE prototypes or standards." S8.4 adds one default preset component per non-residential space type of §2.2, *Office Preset*, *Core Preset*, *Retail Preset*, *Mall Preset*, *Kitchen Preset*, *Dining Preset*, *Operating Theatre Preset*, *Clean Corridor Preset*, *Dirty Corridor Preset*, *Clinical Support Preset*, *Care Bedroom Preset*, *Care Communal Preset*, *Lobby Preset*, *Activity Hall Preset*, and *Service Preset*, with the same inputs; *Gas Equipment* is in W/m², and *Ventilation* and *Infiltration* are in 1/h. Their inputs and outputs are listed in [pipeline.md](../architecture/pipeline.md#grasshopper-components-through-s84).
- [x] Step 8: Append the S8.4 checklist at the end of
docs/development/grasshopper-smoke-test.md
Append this section after the last line of the file (after the S8.3 checklist), separated by one blank line, so that it nests under ## Stage checklists like the S2–S8.3 checklists:
### S8.4 checklist — non-residential programs, office plate, and cafeteria (`v0.8.3`)
S8.4 adds fifteen default preset components in *1 Program* for the non-residential space types ([ADR-014](../decisions/ADR-014-program-types-and-department-zoning.md)) and two plan generator components in *2 Generate* for the office plate (`SYN-TYP-017`) and the cafeteria (`SYN-TYP-016`) ([ADR-006](../decisions/ADR-006-plan-generation-mechanism.md)). No existing component, input, output, or GUID changes.
#### Toolbar and icons
- [ ] *1 Program* shows, after *Stair Preset*, *Office Preset*, *Core Preset*, *Retail Preset*, *Mall Preset*, *Kitchen Preset*, *Dining Preset*, *Operating Theatre Preset*, *Clean Corridor Preset*, *Dirty Corridor Preset*, *Clinical Support Preset*, *Care Bedroom Preset*, *Care Communal Preset*, *Lobby Preset*, *Activity Hall Preset*, and *Service Preset*, each with its own orange icon.
- [ ] *2 Generate* shows *Office Plate* (a plate around a dark core) and *Cafeteria* (a dark kitchen band above a dining field with tables), each with its own green icon.
- [ ] Each preset component works with nothing connected, outputs one *Preset*, and shows the remark "Illustrative values; not sourced from DOE prototypes or standards."
- [ ] *Kitchen Preset* inputs, in order: Name (Example Kitchen), WWR (0.1), Conditioned (True), Heating (18), Cooling (26), Occupancy (0.05), Occupancy Schedule, Lighting (12), Lighting Schedule, Equipment (30), Equipment Schedule, Gas Equipment (40), Gas Equipment Schedule, Ventilation (15), Ventilation Schedule, Infiltration (0.3), Infiltration Schedule; the descriptions give W/m² for *Gas Equipment* and 1/h for *Ventilation*.
- [ ] *Operating Theatre Preset* defaults: WWR 0, Heating 20, Cooling 22, Ventilation 20; *Care Bedroom Preset*: Heating 22, Cooling 25.
- [ ] *Office Plate* (OPlate) inputs, in order, with their defaults: Plate Length 42, Plate Depth 30, Core Length 18, Core Depth 6, Floor Height 3.8, Orientation 0, Office, Core, Preview Location; one output, *Plan*.
- [ ] *Cafeteria* (Cafe) inputs: Length 30, Dining Depth 15, Kitchen Depth 8, Floor Height 4, Orientation 0, Kitchen, Dining, Preview Location; one output, *Plan*.
- [ ] With a preset input unconnected, each generator shows Grasshopper's missing-input warning and outputs nothing (D-065); *Office Preset* on the *Core* input gives `Error PresetSpaceTypeMismatch`.
#### Default plans
1. Place *Office Plate* with *Office Preset* and *Core Preset* connected; connect *Inspect* to its *Plan*.
- [ ] No runtime message; zones `SC` (*Service Core*, 108 m²) and `OF` (*Office*, 1152 m²) in a 42 m × 30 m plate; the preview shows the office as a ring around the core at x = 12–30 m, y = 12–18 m.
- [ ] 4 windows, 218.88 m² of glazing (63.84 north, 63.84 south, 45.6 east, 45.6 west in *Validate* on its *No Simplification* floor); the core has no outdoor wall and no window.
- [ ] The *Inspect* report equals the snapshot `tests/Lod.Generators.Tests/OfficePlate/Snapshots/office-plate-canonical.txt`.
2. Place *Cafeteria* with *Kitchen Preset* and *Dining Preset* connected; connect *Inspect*.
- [ ] No runtime message; zones `KS` (*Kitchen and Servery*, 240 m², y = 15–23 m) and `DI` (*Dining*, 450 m², y = 0–15 m).
- [ ] 6 windows, 114.4 m² of glazing (12 north, 48 south, 27.2 east, 27.2 west).
- [ ] The *Inspect* report equals `tests/Lod.Generators.Tests/Cafeteria/Snapshots/cafeteria-canonical.txt`.
#### Combinations
3. Connect each plan to the four plan simplifiers (*Perimeter Core* Depth 4.57) and each floor three times, Multipliers 1, 3, 1, to the four floor aggregators.
- [ ] Office: *Semantic Merge* gives two zones, the office still a ring; *Perimeter Core* gives `P-North`, `P-East`, `P-South`, `P-West`, `CORE` (685.4596 m², no outdoor walls); *Single Zone per Floor* gives one zone `FLOOR` without a hole and 4 windows.
- [ ] Cafeteria: *Perimeter Core* gives `P-North` (kitchen only), `P-South` (dining only), `P-East`, `P-West`, `CORE`, all `Mixed`.
- [ ] For all 16 combinations of each plan, *Validate* gives Passed = True (only `ConditionedFloorArea` may read `note`), no aggregator shows a warning, and *Convert2BEM* with Override = False converts without an error; *Space Types* lists `Core`, `Office`, `Kitchen`, `Dining`, or `Mixed`.
- [ ] *Convert2BEM* *Zones* of `OF` is one closed Brep with inner walls around the core; its floor and ceiling Breps are single faces with an inner loop for the core; every zone Brep has a positive volume (D-091).
#### Rotated placement
4. Put each plan through *Transform Plan* with a 30° rotation and a move by 5.3, −2.1, 0, then through each simplifier and aggregator (Multipliers 1, 3, 1); repeat with 131.4° and 15.08, −153.9, 0.
- [ ] *Validate* gives Passed = True for all 16 combinations of each placement, with the same total glazing as the unrotated plan.
#### Parameters
5. Change inputs one at a time and set each back afterwards.
- [ ] *Office Plate* Core Length 40: `Error InvalidParameter: CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.` and no plan; Core Length 36 with Core Depth 24 (exactly 3 m on every side) gives a plan.
- [ ] *Office Plate* Plate Depth 0: `Error InvalidParameter: PlateDepth must be positive, got 0.`
- [ ] *Cafeteria* Kitchen Depth 15: `Error InvalidParameter: KitchenDepth 15 m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.` and no plan.
- [x] Step 9: Check the links and commit
git grep -n "grasshopper-components-through-s83" -- docs/architecture docs/research
git grep -c "grasshopper-components-through-s84" -- docs/architecture/pipeline.md docs/research/program-presets.md
git grep -c -e "Implemented (S8.4)" -e "### Pipeline consequences of S8.4" -- docs/decisions/ADR-006-plan-generation-mechanism.md
git add docs/architecture/pipeline.md docs/architecture/convert2bem.md docs/architecture/domain-model.md docs/decisions/ADR-002-geometry.md docs/decisions/ADR-006-plan-generation-mechanism.md docs/research/program-presets.md docs/development/grasshopper-smoke-test.md
git commit -m "docs(architecture): document the non-residential presets, office plate, and cafeteria"
Expected: no output from the first command; docs/architecture/pipeline.md:1, docs/research/program-presets.md:1 from the second; and docs/decisions/ADR-006-plan-generation-mechanism.md:3 from the third.
Task 11: Smoke test (the controller headless, D-056; viewport items for a person)¶
Files: none (the result goes into docs/development/grasshopper-smoke-test.md and the body of the close-out commit, Task 12)
- [x] Step 1: Build the branch and check that it is the tested code
dotnet build BEMGen.sln -c Release
git rev-parse HEAD
git diff --stat f841f0cf670cfff50f18c22e5ba39c0e43e78603 HEAD -- src tests scripts
Expected: Build succeeded., 0 Warning(s), 0 Error(s); note the SHA; the last command prints nothing: src/, tests/, and scripts/ are identical to the verified reference f841f0c on which the controller ran the headless session (D-056) whose values Step 2 lists (note 1). f841f0c is the tip of the local reference branch ref/s8.4 in the controller's repository. If the command prints anything, or the commit is not available, stop: the record of Step 2 does not apply, and the session is run again on this branch.
- [x] Step 2: The S8.4 checklist and its record
The controller ran the checks that do not need a viewport as a scripted headless Grasshopper session (D-056: Rhino 8 /nosplash /netcore with RHINO_PACKAGE_DIRS pointing at src/Lod.Grasshopper/bin/Release/net7.0/, components placed by GUID, states, messages, outputs, data trees, bounding boxes, Brep face, inner-loop, and validity counts, Brep.GetVolume, and BrepFace.NormalAt at each face's domain centre written to a file) on the verified reference f841f0c in Rhino 8.25.25314.11001 (.NET 8.0.31); the reference assembly reported the informational version 0.8.2+f841f0cf670cfff50f18c22e5ba39c0e43e78603 (the bump to 0.8.3 comes in Task 12). "The presets" are the default preset components of the family with nothing connected; "1, 3, 1" means a simplified floor given three times to an aggregator with those multipliers. Observed, and all passed:
- Registration. 46 components and 6 parameters, each with a 24 × 24 icon, plus a 24 × 24 assembly icon. In 1 Program, all primary: Activity Hall Preset Hall
6bf00cd3-70b9-4337-8b80-70fc00d79666, Care Bedroom Preset CBed8447ea38-faec-4630-96dd-92569cd7d0d8, Care Communal Preset CComma3000f8c-633f-4a54-9fb8-1a4109e337bb, Clean Corridor Preset Cleanc199a986-28b8-41c7-b260-5f4581b7362b, Clinical Support Preset ClinS274e0ac9-de95-4bd4-8028-25d74bfd1e4b, Core Preset Coreca44b061-1ced-4ae3-afed-d806e6db2b7c, Dining Preset Dinec8b05fa2-330a-4e8a-acaa-e57ca57b4fd9, Dirty Corridor Preset Dirty0e61c86e-5981-4a11-b922-52650757e4a9, Kitchen Preset Kitch658e05dc-c935-4518-bc99-512546153e1e, Lobby Preset Lobby395aebb3-3e49-4814-93ab-388da9de2360, Mall Preset Malldbd514d5-7553-4000-b10c-bca748e3b301, Office Preset Office4160f791-07dc-4529-9986-1c9ba7d21a98, Operating Theatre Preset OTeb55b6d5-08f1-4a5e-9598-13951c34c2a4, Retail Preset Retailb760860d-05c9-4cbe-a22f-ddd2e38e4d64, Service Preset Serv60251c8e-ad4b-4ef5-8f68-64f4c6bf6f8b; in 2 Generate, all primary: Cafeteria Cafe95956724-ddbc-48ce-9874-0ec6c244c8feand Office Plate OPlate783cefaa-b836-4dbc-b16d-8e74820ed361. Every other component and parameter as in S8.3. - Inputs. Activity Hall Preset: Name (N)
Example Activity Hall, WWR 0.2, Conditioned (Cond) True, Heating (H) 18, Cooling (C) 25, Occupancy (Occ) 0.1, Occupancy Schedule (optional), Lighting (Lt) 12, Lighting Schedule, Equipment (Eq) 2, Equipment Schedule, Infiltration (Inf) 0.3, Infiltration Schedule; Kitchen Preset with the two more loads after Equipment (30): Gas Equipment (Gas) 40, Gas Equipment Schedule, Ventilation (Vent) 15, Ventilation Schedule, then Infiltration 0.3; the other presets likewise with their ADR-014 values; Office Plate and Cafeteria with the inputs and defaults of Task 9. - Preset defaults. Every preset component, with nothing connected, shows only the remark
Illustrative values; not sourced from DOE prototypes or standards.and outputs one preset, for examplePreset Example Office (Office, WWR 0.4),Preset Example Operating Theatre (OperatingTheatre, WWR 0),Preset Example Lobby (Lobby, WWR 0.5), andPreset Example Core (Core, WWR 0). - Office plate defaults. Office Plate with Office Preset and Core Preset → Inspect; → No Simplification → Stack Floors (one floor) → Convert2BEM; and → Semantic Merge, Perimeter Core (default depth), and Single Zone per Floor: no runtime message. Plan box (0, 0, 0)–(42, 30, 3.8); Inspect
footprint area=1260.000000 floorHeight=3.800000 orientation=0.000000; floors of 2, 5, and 1 zones; Boundaries Adiabatic 8, Ground 2, Outdoors 6; Windows 4, all on the office (none in the core's branch). Zones: the core a valid closed Brep of 6 faces, volume 410.4, box (12, 12, 0)–(30, 18, 3.8); the office a valid closed Brep of 10 faces with 2 inner loops, volume 4377.6 (4788 − 410.4), box (0, 0, 0)–(42, 30, 3.8). The office's walls toward the core face into the core ((0, −1, 0) at y = 12, (1, 0, 0) at x = 30, (0, 1, 0) at y = 18, (−1, 0, 0) at x = 12), its outer walls outward, floors (0, 0, −1), ceilings (0, 0, 1), windows as their walls. Perimeter Core → Stack Floors → Convert2BEM: 5 valid closed Breps of 6 faces, volumes 650.009 (north and south), 441.617 (east and west), and 2604.746 (CORE, box (4.57, 4.57, 0)–(37.43, 25.43, 3.8)). Single Zone per Floor (three times) → Single Zone Building (1, 3, 1) → Convert2BEM and Validate: 5 Zones Breps of 6 faces and volume 4788 at z = 0–3.8 … 15.2–19; Validate Passed =True; the preview one merged solid (0, 0, 0)–(42, 30, 19) and 20 window Breps. - Cafeteria defaults. Cafeteria with Kitchen Preset and Dining Preset, the same components: no runtime message. Plan box (0, 0, 0)–(30, 23, 4); Inspect
footprint area=690.000000 floorHeight=4.000000 orientation=0.000000; floors of 2, 5, and 1 zones; Boundaries Adiabatic 2, Ground 2, Outdoors 8; Windows 6 (3 per zone). Zones: kitchen and servery volume 960, box (0, 15, 0)–(30, 23, 4); dining volume 1800, box (0, 0, 0)–(30, 15, 4); both valid closed Breps of 6 faces; the shared wall faces (0, −1, 0) from the kitchen and (0, 1, 0) from the dining field. Perimeter Core: 5 valid closed Breps, volumes 464.86, 336.9, 464.86, 336.9, and 1156.478. Single Zone Building (1, 3, 1): 5 Breps of volume 2760; Validate Passed =True; the preview one solid (0, 0, 0)–(30, 23, 20). - Every combination, also rotated. Each plan on its canonical placement, through Transform Plan with a 30° rotation about the world z axis and a move by (5.3, −2.1, 0), and with a 131.4° rotation and a move by (15.08, −153.9, 0), then through each of the four plan simplifiers (Perimeter Core with its default depth), each floor given 1, 3, 1 to each of the four aggregators, each building to Validate: no runtime message, and Validate Passed =
Truefor all 96 buildings. No Brep has a negative volume in any scenario of items 4 to 6. - Parameters and presets. Office Plate Core Length 40: error state,
Error InvalidParameter: CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.; Plate Length 0:Error InvalidParameter: PlateLength must be positive, got 0.; Cafeteria Kitchen Depth 20:Error InvalidParameter: KitchenDepth 20 m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.; a Kitchen Preset on the Office input:Error PresetSpaceTypeMismatch [Office]: Input Office needs a Office preset, got 'Example Kitchen' (Kitchen).; with the preset inputs unconnected, the warning state withInput parameter Of failed to collect dataandInput parameter Co failed to collect data(Office Plate) andInput parameter Ki failed to collect dataandInput parameter Di failed to collect data(Cafeteria), and no output.
Items that a headless session cannot observe stay open for a person (D-056, D-058): the toolbar and canvas icons, the viewport previews (the office ring around the core, the zone colours of the new space types), the Inspect reports compared with the canonical snapshots (the generator tests check them), the zone IDs of Semantic Merge and Perimeter Core (the integration tests check them), the glazing values of Validate, the input descriptions of Kitchen Preset, and Convert2BEM with Override = False on each of the combinations. No example definition is part of S8.4; the pending S4 definition (examples/grasshopper/checkpoint-1-pipeline.gh) stays person-only (D-058).
If Step 1 printed a difference, or a person or a repeated session finds a failure, the stage stops: fix it on this branch with a test (core) or a build plus smoke test (Grasshopper) before continuing. Task 12 records this result in the guide and in the close-out commit, following the guide's "Recording the result".
- [x] Step 3: Merge Slice D (D-004)
git fetch origin
git switch main
git pull --ff-only
git switch docs/architecture-non-residential
git rebase main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
git switch main
git merge --ff-only docs/architecture-non-residential
git branch -d docs/architecture-non-residential
Expected: VERIFY PASSED, with 193 core, 224 generators, and 704 integration tests passed on net8.0. Do not push (D-098).
Stage close-out¶
Task 12: Version 0.8.3, S8.4 records, tag v0.8.3¶
Files:
- Modify: Directory.Build.props, AGENTS.md, README.md, docs/plans/2026-09-30-implementation-roadmap.md, docs/development/grasshopper-smoke-test.md, docs/decisions/decision-log.md, docs/plans/2026-10-02-s8.4-non-residential-programs.md (this plan: checkboxes and status, D-051)
Interfaces:
- Consumes: the merged Slices A–D; the smoke-test record of Task 11; BuildInfoTests.InformationalVersionStartsWithTheAssemblyVersion (S0), which keeps passing after the bump.
- Produces: the tag v0.8.3; the S8.4 records: the project state, the status, the roadmap's Progress line and detailed-plans row, the smoke-test result in the guide, and the decision-log entries D-099 (SYN-TYP-* IDs, which the precedent page of Task 1 states as a provisional controller decision of S8.4), D-100 (at most four arms on the hub-and-arms layout, ADR-006 Upper bounds), and D-101 (S8.4 complete).
The decision-log entries of Step 8 and the roadmap Progress line of Step 6 have one field filled in when this task runs: <date>, the date the stage is closed (ISO format, YYYY-MM-DD). The entries are decided by the controller (unsupervised run, D-098) and Provisional. The close-out uses its own branch, as the S1–S8.3 close-outs do.
- [x] Step 1: Create the branch and run the gate
git switch -c chore/repo-s8.4-close-out main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
Expected: VERIFY PASSED, with 193 core, 224 generators, and 704 integration tests passed on net8.0.
- [x] Step 2: Bump the version in
Directory.Build.props
Replace
<Version>0.8.2</Version>
with
<Version>0.8.3</Version>
Full file after this step:
<Project>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<Deterministic>true</Deterministic>
<Version>0.8.3</Version>
<Authors>Environmental Systems Lab</Authors>
<Product>BEMGen</Product>
</PropertyGroup>
<!-- Records and init accessors on netstandard2.0 (ADR-001). -->
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(MSBuildThisFileDirectory)src\Shared\IsExternalInit.cs" Link="Polyfills\IsExternalInit.cs" />
</ItemGroup>
</Project>
- [x] Step 3: Check the version test
dotnet test tests/Lod.Core.Tests -c Release --filter "FullyQualifiedName~Lod.Core.Tests.Common.BuildInfoTests"
Expected: Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1 for net8.0.
- [x] Step 4: Commit the version
git add Directory.Build.props
git commit -m "build(repo): set version 0.8.3"
- [x] Step 5: Update
AGENTS.mdandREADME.md
In AGENTS.md, replace everything between the heading ## Project state and the heading ## Commands (both headings stay) with:
Checkpoint 1 (D-030) is reached: stages S0–S4 of the [implementation roadmap](docs/plans/2026-09-30-implementation-roadmap.md) are complete (tag `v0.4.0`), and so are the revision stages S4.1 (tag `v0.4.1`, D-064) and S4.2 (tag `v0.4.2`, D-073), stage S5 (tag `v0.5.0`, D-079), and sub-stages S8.1 (tag `v0.8.0`), S8.2 (tag `v0.8.1`), S8.3 (tag `v0.8.2`, D-084), and S8.4 (tag `v0.8.3`, D-101). The pipeline runs end to end for eleven plan generators: program presets (the residential `ExampleResidentialPresets` and, for the non-residential departments of [ADR-014](docs/decisions/ADR-014-program-types-and-department-zoning.md), `ExampleNonResidentialPresets`, one typed preset and default preset component per space type) → a plan generator (one `PlanGenerator` subclass per typology family with typed parameter and preset records, [ADR-006](docs/decisions/ADR-006-plan-generation-mechanism.md); `SYN-TYP-001` to `009` are the *High-Density Housing* families `HDHM-TYP-001` to `009`, D-099: `LinearPlanGenerator`, the double-sided internal corridor bar `SYN-TYP-009`; the bar families `StairBayBarGenerator` 001, `StairPairGenerator` 008, `GalleryBarGenerator` 002 with its gallery not modelled, and `TerraceRowGenerator` 006 with one dwelling zone per plot and storey, D-085; the non-rectangular families `PointPlateGenerator` 007, a square plate whose core has no outdoor wall, `WingedBandGenerator` 003, a comb, and `OpenCourtGenerator` 004, a U; `EnclosedCourtGenerator` 005, four wings around a court closed on all four sides, the first footprint with a hole; and the non-residential `OfficePlateGenerator` `SYN-TYP-017`, whose office work area is one ring-shaped zone around a central `Core` zone, and `CafeteriaGenerator` `SYN-TYP-016`, a `Kitchen` zone along the whole north edge of a larger `Dining` zone, each zoned one zone per department, D-097; one typed preset per space type, D-061; optionally rotated and moved by `PlanTransform`, D-070) → plan simplifiers (`NoSimplification`, `SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`) → floor aggregators (`Stack`, `StackedFloorZoneMultiplier`, `SingleZonePerFloorType`, `SingleZoneBuilding`, D-071; floors and ceilings designated on the assembled stack, D-068) → validation → `Convert2BEM` (heat transfer between zones an export option, off by default, D-069), as described in [docs/architecture/pipeline.md](docs/architecture/pipeline.md). Every generator places one centred window per outdoor wall (D-026), except on a wall shorter than 1 m or where the window would be narrower or lower than 0.3 m (`GeometryLimits`, info `WindowOmitted`, [ADR-011](docs/decisions/ADR-011-minimum-wall-and-window-geometry.md)). Wherever zones are merged and walls are rebuilt (`SemanticMerge`, `PerimeterCore`, `SingleZonePerFloor`, `SingleZonePerFloorType`, `SingleZoneBuilding`), every outdoor wall carries one centred window with the glazed area of the source windows it covers (`CentredWindows`, D-079, [ADR-012](docs/decisions/ADR-012-centred-windows.md)); the other steps keep their input's windows. Merged zones are unions of their sources as tiles that keep the sources' own vertices, so they may have holes and stay exact on rotated plans, and vertices on edges are recognised at their original positions (D-088, [ADR-002](docs/decisions/ADR-002-geometry.md)). Every step supports footprints with holes: a court is outside the building, its edges are outdoor walls facing into it, every footprint edge, court edges included, must be covered by outdoor zone edges (ADR-002), and `PerimeterCore` gives the court façades their own zones `P-Court-<Bin>` around a core that is a ring ([ADR-008](docs/decisions/ADR-008-perimeter-core-corners.md)); `PerimeterCore` needs every part of a footprint, including the wings between the outer and the court façades, to be wider than twice its depth and otherwise fails with `PlanTooNarrow` (D-087). Vertical aggregation follows [ADR-013](docs/decisions/ADR-013-vertical-aggregation-methods.md), which supersedes [ADR-009](docs/decisions/ADR-009-vertical-aggregation.md) where it differs. In *Convert2BEM* and the previews, zone solids are outward-oriented and every surface and window faces out of its zone (D-091). ADR-006, ADR-011, ADR-012, ADR-013, ADR-014, and the S4.2, S5, S8.1, S8.2, S8.3, and S8.4 plans were accepted by the controller and await the owner's reading (D-075, D-080, D-084, D-098). In Grasshopper every component has an icon drawn by `scripts/make_icons.py` (D-063), and the generator, *Transform Plan*, simplifier, and aggregator components have a *Preview Location* input that moves only their preview (D-062). Until the owner says otherwise, changes need not keep backward compatibility, but component GUIDs never change (D-064).
The precedent set is the four-source family set `SYN-TYP-001` to `SYN-TYP-019` of every program type, housing and non-residential ([docs/research/precedents/syn-typologies.md](docs/research/precedents/syn-typologies.md), D-093, D-094); `SYN-TYP-013` is set aside (D-096), non-residential floors are zoned one zone per department (D-097, [ADR-014](docs/decisions/ADR-014-program-types-and-department-zoning.md)), and BEMGen documents use the `SYN-TYP-*` IDs (D-099). The families `017` and `016` were built in S8.4; `010`, `014`, `018`, `019` (S8.5), `015`, `011` (S8.6), and `012` (S8.7) are specified in ADR-006 and built before S6 (D-098), the hub-and-arms families with at most four arms because the shared layout is axis-aligned (D-100).
Not implemented yet: the four-source families of S8.5–S8.7, `Lod.Export` and Convert2IDF (S6), and Grasshopper UX and release (S9). The precedent study of S7 was carried out in a separate repository (D-082); its nine families are paraphrased in [docs/research/precedents/hdhm-typologies.md](docs/research/precedents/hdhm-typologies.md). A zone only about 1 m wide on a rotated plan can fail validation after a merging simplifier, a known limit of clipping on the grid (D-090). The example program presets hold illustrative values, not DOE prototype values, and the `Convert2BEM` output layout stays provisional until a ClimateStudio reference definition exists (D-023).
In README.md, replace everything between the heading ## Status and the heading ## What it does (both headings stay) with:
Checkpoint 1 reached: stages S0–S4 of the [implementation roadmap](docs/plans/2026-09-30-implementation-roadmap.md) are complete (tag `v0.4.0`), followed by the revision stages S4.1 (tag `v0.4.1`) and S4.2 (tag `v0.4.2`), stage S5 (tag `v0.5.0`), and sub-stages S8.1 (tag `v0.8.0`), S8.2 (tag `v0.8.1`), S8.3 (tag `v0.8.2`), and S8.4 (tag `v0.8.3`). In Rhino 8 the plugin runs the whole pipeline for eleven plan generators: program presets with conditioning (a default preset component per space type, housing and non-residential); the linear plan generator and the eight other typologies of the *High-Density Housing* precedent study (`SYN-TYP-001`–`009`, ADR-006), the bars *Stair Bay Bar*, *Stair Pair*, *Gallery Bar*, and *Terrace Row*, the non-rectangular *Point Plate*, *Winged Band*, and *Open Court*, *Enclosed Court*, whose footprint has the court as a hole, and the non-residential *Office Plate* (`SYN-TYP-017`, a work-area ring around a central core) and *Cafeteria* (`SYN-TYP-016`, a kitchen and servery edge beside a dining field), each zoned one zone per department (D-097, ADR-014), each with one preset input per space type and explicit windows (none on a wall shorter than 1 m or for a window below 0.3 m, ADR-011); *Transform Plan* to rotate and move a plan; four plan simplifiers (Z0–Z3); four floor aggregators (Stack, StackedFloorZoneMultiplier, SingleZonePerFloorType, SingleZoneBuilding) that designate floors and ceilings on the assembled stack and warn about unsupported storeys; invariant validation; and `Convert2BEM`, which writes heat transfer between zones only when asked (off by default). Wherever zones are merged and walls are rebuilt, every outdoor wall carries one centred window with the glazed area of the windows it covers, so glazing is conserved per wall, façade, orientation, and building (D-079); merged zones stay exact on rotated plans (D-088); every simplifier and aggregator supports footprints with holes, with the court façades as outdoor walls; and zone solids, surfaces, and windows are oriented out of their zones (D-091). Every component has an icon, and every component with a viewport preview has a *Preview Location* input that moves only its preview. The pipeline is described in [docs/architecture/pipeline.md](docs/architecture/pipeline.md); the example definition `examples/grasshopper/checkpoint-1-pipeline.gh`, which wires every simplifier into every aggregator, is still to be built by a person. Next: S8.5–S8.7 (the remaining four-source families, D-098), then Convert2IDF (S6) and Grasshopper UX and release (S9). The `Convert2BEM` output stays provisional until a ClimateStudio reference definition exists (D-023), and the example program presets hold illustrative values, not DOE prototype values.
In README.md, make this replacement in the "Planned layout" block; the "Replace" text occurs exactly once in the file.
- Replace
Lod.Generators/ plan generators, one per typology family (the linear plan, the S8.1 bars, the S8.2 non-rectangular families, and the S8.3 enclosed court)
with
Lod.Generators/ plan generators, one per typology family (the linear plan, the S8.1 bars, the S8.2 non-rectangular families, the S8.3 enclosed court, and the S8.4 office plate and cafeteria)
- [x] Step 6: Update the roadmap
In docs/plans/2026-09-30-implementation-roadmap.md, make these replacements, in order; each "Replace" text occurs exactly once in the file. The first adds the S8.4 Progress line below the S8.3 Progress line (the last > **Progress:** line), with a > separator line between them; the second adds the S8.4 row of the detailed-plans table.
- Replace
> **Progress:** 2026-10-02 · S8.3 Enclosed courtyard and footprints with holes complete (tag `v0.8.2`, local until the owner pushes it, D-084); next: S6 and S9. New generator and component *Enclosed Court* (005, four wings of stair bays around a court closed on all four sides), the first footprint with a hole; the surface builder requires every footprint edge, court edges included, to be covered by outdoor zone edges; *Perimeter Core* zones the court façades in their own zones `P-Court-<Bin>` around a ring core, with its width precondition on every wing (D-087); *No Simplification*, *Semantic Merge*, *Single Zone per Floor*, the four floor aggregators, and validation needed no change; every simplifier and aggregator validates the enclosed court, also rotated and moved; zone solids are outward-oriented and surfaces and windows face out of their zones in *Convert2BEM*, which repairs every plan released up to `v0.8.1` (D-091); existing snapshots byte-identical, one new; 158 core, 181 generator, and 601 integration tests pass on `net8.0`.
with
> **Progress:** 2026-10-02 · S8.3 Enclosed courtyard and footprints with holes complete (tag `v0.8.2`, local until the owner pushes it, D-084); next: S6 and S9. New generator and component *Enclosed Court* (005, four wings of stair bays around a court closed on all four sides), the first footprint with a hole; the surface builder requires every footprint edge, court edges included, to be covered by outdoor zone edges; *Perimeter Core* zones the court façades in their own zones `P-Court-<Bin>` around a ring core, with its width precondition on every wing (D-087); *No Simplification*, *Semantic Merge*, *Single Zone per Floor*, the four floor aggregators, and validation needed no change; every simplifier and aggregator validates the enclosed court, also rotated and moved; zone solids are outward-oriented and surfaces and windows face out of their zones in *Convert2BEM*, which repairs every plan released up to `v0.8.1` (D-091); existing snapshots byte-identical, one new; 158 core, 181 generator, and 601 integration tests pass on `net8.0`.
>
> **Progress:** <date> · S8.4 Non-residential programs, office plate and cafeteria complete (tag `v0.8.3`; `main` and the tag pushed after the fresh-clone gate, D-098); next: S8.5. The four-source records are pinned (`v0.3.1-research`, `7538ed3`) and paraphrased in `syn-typologies.md`, and BEMGen documents use the `SYN-TYP-*` IDs (D-099); the study covers every program type (D-094) in the brief, spec, README, AGENTS.md, and roadmap; ADR-014 adds twelve non-residential space types (`Core`, `Lobby`, and `Service` reused), fifteen illustrative presets in `ExampleNonResidentialPresets`, their default preset components, and the department zoning of every new family (D-097); ADR-006 specifies all nine new families, the hub-and-arms families with at most four arms (D-100); new generators and components *Office Plate* (017, the first zone with a hole in a footprint without one, needing no pipeline change) and *Cafeteria* (016); existing snapshots byte-identical, two new; 193 core, 224 generator, and 704 integration tests pass on `net8.0`.
- Replace
| S8.3 | [2026-10-02-s8.3-enclosed-courtyard.md](2026-10-02-s8.3-enclosed-courtyard.md) | 158 core, 181 generators, 601 integration |
with
| S8.3 | [2026-10-02-s8.3-enclosed-courtyard.md](2026-10-02-s8.3-enclosed-courtyard.md) | 158 core, 181 generators, 601 integration |
| S8.4 | [2026-10-02-s8.4-non-residential-programs.md](2026-10-02-s8.4-non-residential-programs.md) | 193 core, 224 generators, 704 integration |
The S8.4 edits of Task 3 (the S8.4–S8.7 rows, the S8 section, the layout lines) stay as they are.
- [x] Step 7: Record the smoke test in
docs/development/grasshopper-smoke-test.md
Append this section at the end of docs/development/grasshopper-smoke-test.md (after the S8.4 checklist of Task 10), separated by one blank line; it records Task 11 as the guide's "Recording the result" asks, and the commit body of Step 9 repeats it:
#### Result (S8.4)
Run by the controller as a scripted headless Grasshopper session (D-056) on 2026-10-02, on the verified reference of this sub-stage (`f841f0cf670cfff50f18c22e5ba39c0e43e78603`, whose `src/`, `tests/`, and `scripts/` are identical to the executed branch): Rhino 8.25.25314.11001, .NET 8.0.31, `BEMGen.gha` built in Release with 0 warnings, *BEMGen Info* version `0.8.2+f841f0cf670cfff50f18c22e5ba39c0e43e78603` (the version becomes 0.8.3 at the close-out).
- Passed: the 15 default preset components in *1 Program* and the 2 generator components in *2 Generate*, all primary, with the GUIDs of the S8.4 plan and 24 × 24 icons; every preset builds with only the remark "Illustrative values; not sourced from DOE prototypes or standards." (for example `Preset Example Office (Office, WWR 0.4)`, `Preset Example Operating Theatre (OperatingTheatre, WWR 0)`, `Preset Example Lobby (Lobby, WWR 0.5)`).
- Passed: *Office Plate* defaults: box (0, 0, 0)–(42, 30, 3.8), footprint 1260 m²; *Semantic Merge* 2 zones, *Perimeter Core* 5, *Single Zone per Floor* 1; *Stack Floors* (one floor) boundaries Adiabatic 8, Ground 2, Outdoors 6; 4 windows on the office façades and none on the core. *Zones*: the core of 6 faces, solid, valid, volume 410.4; the office of 10 faces, solid, valid, 2 inner loops, volume 4377.6 (4788 − 410.4). The office walls toward the core face into the core, the outer walls outward, floors down, ceilings up, windows as their walls. *Perimeter Core*: 5 valid solids, all with positive volume. *Single Zone Building* (1, 3, 1): *Validate* Passed = `True`, the preview one solid (0, 0, 0)–(42, 30, 19).
- Passed: *Cafeteria* defaults: box (0, 0, 0)–(30, 23, 4), footprint 690 m²; *Stack Floors* boundaries Adiabatic 2, Ground 2, Outdoors 8; 6 windows; *Zones* of volume 960 (kitchen and servery) and 1800 (dining), both valid solids of 6 faces; *Perimeter Core* 5 valid solids; *Single Zone Building* (1, 3, 1): *Validate* Passed = `True`, the preview one solid (0, 0, 0)–(30, 23, 20).
- Passed: all 96 combinations (the two families, on the canonical plan and rotated by 30° and moved by (5.3, −2.1, 0) and rotated by 131.4° and moved by (15.08, −153.9, 0), each through the four simplifiers and the four aggregators with 1, 3, 1): *Validate* Passed = `True`, no error, and no negative volume.
- Passed: *Office Plate* Core Length 40: `Error InvalidParameter: CoreLength 40 m leaves an office depth of 1 m at the west and east façades; SYN-TYP-017 needs at least 3 m of work area on every side of the core.`; Plate Length 0: `Error InvalidParameter: PlateLength must be positive, got 0.`; *Cafeteria* Kitchen Depth 20: `Error InvalidParameter: KitchenDepth 20 m must be smaller than DiningDepth 15 m: in SYN-TYP-016 the dining field is the larger part of the plan.`; a *Kitchen Preset* on the *Office* input: `Error PresetSpaceTypeMismatch [Office]: Input Office needs a Office preset, got 'Example Kitchen' (Kitchen).`; unconnected preset inputs: Grasshopper's missing-input warnings.
- Not performed, open for a person (D-058): the viewport look of the previews, the icons in the toolbar, the *Inspect* reports compared with the snapshots (the generator tests check them), and example definitions.
- [x] Step 8: Append the decision-log entries (template; fill
<date>now)
Append at the end of docs/decisions/decision-log.md, after a blank line, the three entries below, each separated from the next by one blank line. The log ends with D-098 when this plan is approved, so the entries are D-099, D-100, and D-101; if the log ends with another number, use the next free numbers, change the references between the entries (D-101 cites D-099 and D-100; AGENTS.md of Step 5 cites D-099, D-100, and D-101; the Progress line of Step 6 cites D-099 and D-100) to match, and say so in the commit body.
### D-099 — BEMGen documents use the `SYN-TYP-*` family IDs
- **Date:** <date> · **Decided by:** controller (unsupervised run, D-098) · **Status:** Provisional
- Why: the precedent set is the four-source family set (D-093), whose IDs `SYN-TYP-001` to `SYN-TYP-019` cover every family; `SYN-TYP-001` to `SYN-TYP-009` carry the nine *High-Density Housing* families `HDHM-TYP-001` to `HDHM-TYP-009` with the same organisation and rule numbering.
- From S8.4 on, BEMGen documents name families by their `SYN-TYP-*` IDs and give the `HDHM-TYP-*` lineage for `001` to `009` ([syn-typologies.md](../research/precedents/syn-typologies.md)). `hdhm-typologies.md` stays as the issued baseline and is not updated further. Component descriptions and ADR-006 sections written in S8.1–S8.3 may keep their `HDHM-TYP-*` IDs as long as the `SYN-TYP-*` ID is also given; descriptions are updated when a component is next changed, and no GUID changes.
### D-100 — The hub-and-arms families have at most four arms
- **Date:** <date> · **Decided by:** controller (unsupervised run, D-098) · **Status:** Provisional
- Why: the families of S8.5 that branch from a hub (`SYN-TYP-010`, `014`, `018`, `019`) share one layout, `HubArmsLayout`, which is axis-aligned in the plan frame of ADR-002, so its arms can only leave the hub on its four sides.
- The hub-and-arms families are limited to 3–4 arms, `018` to 2–4, as [ADR-006](ADR-006-plan-generation-mechanism.md) specifies: 3–4 lobes (`010`), 3–4 branches (`014`), 2–4 bedroom wings (`018`), and 2–3 halls (`019`, whose support wing takes one more arm, so 3–4 arms in all); a larger count is `InvalidParameter`. This is a limit of BEMGen's layout, not of the families, which set no bound (ADR-006: no family has an upper bound). It is revisited in S8.5 if a family needs a radial layout with arms at other angles.
### D-101 — S8.4 complete
- **Date:** <date> · **Decided by:** controller (unsupervised run, D-098) · **Status:** Provisional
- Sub-stage S8.4 (D-098) is complete and tagged `v0.8.3`; after its gate and a fresh clone of the tag pass `scripts/verify.ps1`, `main` and `v0.8.3` are pushed (D-098). The four-source records of `typology-synthesizer` are pinned at `v0.3.1-research` (`7538ed3`) with their SHA-256 and paraphrased in `docs/research/precedents/syn-typologies.md` (D-093, D-099). The scope of D-094 is in the research brief (question 2, §4), the repository spec, README, AGENTS.md, and the roadmap (S8.4–S8.7 before S6; S9 after S6 and S8.7).
- [ADR-014](ADR-014-program-types-and-department-zoning.md), accepted by the controller under D-098 and pending the owner's reading, appends twelve `SpaceType` values (`Office`, `Retail`, `Mall`, `Kitchen`, `Dining`, `OperatingTheatre`, `CleanCorridor`, `DirtyCorridor`, `ClinicalSupport`, `CareBedroom`, `CareCommunal`, `ActivityHall`), reuses `Core`, `Lobby`, and `Service`, and gives each of the fifteen types one illustrative, conditioned preset in `ExampleNonResidentialPresets` (not DOE or standard values, D-023; ventilation only for the kitchen and the operating theatre; retail schedules with their own Saturday and Sunday) and a default preset component in *1 Program*, with icons. It also sets which rooms form one zone in every new family (D-097). ADR-006 specifies all nine new families (010, 011, 012, 014–019) with controller design defaults (D-098), the hub-and-arms families with at most four arms (D-100).
- New generators and components in *2 Generate*, with icons: *Office Plate* (`OfficePlateGenerator`, `SYN-TYP-017`, OPlate, `783cefaa-b836-4dbc-b16d-8e74820ed361`): an `Office` ring around a central `Core` zone without outdoor walls, at least 3 m of work area on every side. *Cafeteria* (`CafeteriaGenerator`, `SYN-TYP-016`, Cafe, `95956724-ddbc-48ce-9874-0ec6c244c8fe`): a `Kitchen` zone along the whole north edge of a larger `Dining` zone. The office ring is the first zone with a hole in a footprint without one; the surface builder, every simplifier, and every aggregator handle it without change. No tolerance or check was relaxed.
- The sixteen existing snapshots are byte-identical; two canonical snapshots are new. 193 core, 224 generator, and 704 integration tests pass on `net8.0`, including both families with every simplifier and aggregator on the canonical plan and on the plan rotated by 30° and moved by (5.3, −2.1) m and rotated by 131.4° and moved by (15.08, −153.9) m.
- Smoke test (D-056): the controller's scripted headless Grasshopper session on the verified reference (`f841f0c`, whose `src/`, `tests/`, and `scripts/` equal the executed branch) in Rhino 8.25.25314.11001 (.NET 8.0.31) passed every performed item: the 15 preset and 2 generator components registered with their GUIDs and 24 × 24 icons; every preset with only its remark; the default office plate (1260 m², core solid of volume 410.4, office solid of 10 faces with 2 inner loops and volume 4377.6, walls toward the core facing into it) and cafeteria; all 96 combinations (both families, three placements, four simplifiers, four aggregators, 1/3/1) with *Validate* passing and no negative volume; and the error cases. It is recorded in `docs/development/grasshopper-smoke-test.md` and the close-out commit; the viewport look, the icons in the toolbar, and example definitions stay open for a person (D-058).
- S8.5 (families 010, 014, 018, 019 on the shared hub-and-arms layout) can start.
Before committing, check that no <date> is left:
git grep -n -F -e "<date>" -- docs/decisions/decision-log.md docs/plans/2026-09-30-implementation-roadmap.md
Expected: no output.
- [x] Step 9: Tick this plan and commit the records
Tick every completed checkbox of this plan and set its status line to > **Status:** Done (controller, D-098; pending the owner's reading) · … (D-051). Leave unticked the steps that run after this commit (Steps 10 and 11 below) and every step of Task 13, and name them in the commit body. Tick only the plan's own step checkboxes: the - [ ] items inside the checklist block of Task 10, Step 8 are file content of grasshopper-smoke-test.md and stay unticked, so the block keeps matching the file. The second -m records the smoke test as docs/development/grasshopper-smoke-test.md prescribes ("Recording the result").
git add AGENTS.md README.md docs/plans/2026-09-30-implementation-roadmap.md docs/development/grasshopper-smoke-test.md docs/decisions/decision-log.md docs/plans/2026-10-02-s8.4-non-residential-programs.md
git commit -m "docs(repo): record s8.4 completion" -m "Smoke test (docs/development/grasshopper-smoke-test.md, S8.4 checklist) by the controller as a scripted headless Grasshopper session (D-056) on 2026-10-02: Rhino 8.25.25314.11001 (.NET 8.0.31), .NET Core runtime, build f841f0cf670cfff50f18c22e5ba39c0e43e78603 (the verified reference; src, tests, and scripts identical to this branch), Version output '0.8.2+f841f0cf670cfff50f18c22e5ba39c0e43e78603', all performed items passed. Not performed, pending a person (D-058): the viewport previews, the icons in the toolbar and on the canvas, and the other items named in Task 11 of the S8.4 plan." -m "Ticked per D-051: every completed checkbox of Tasks 1 to 11 and of Task 12 up to this commit (Steps 1 to 9). Left unticked: Task 12 Step 10 (merge) and Step 11 (tag v0.8.3), which run after this commit, and every step of Task 13 (fresh-clone gate and push), which the controller performs after the tag (D-098). The checklist items inside Task 10 are file content of grasshopper-smoke-test.md and stay unticked."
- [ ] Step 10: Merge (D-004)
git fetch origin
git switch main
git pull --ff-only
git switch chore/repo-s8.4-close-out
git rebase main
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
git switch main
git merge --ff-only chore/repo-s8.4-close-out
git branch -d chore/repo-s8.4-close-out
Expected: VERIFY PASSED, with 193 core, 224 generators, and 704 integration tests passed on net8.0.
- [ ] Step 11: Tag
git tag -a v0.8.3 -m "S8.4 non-residential programs, office plate and cafeteria complete"
git describe --tags
Expected: v0.8.3. Nothing is pushed before Task 13.
Task 13: Fresh-clone gate and push (controller, D-098)¶
Files: none
D-098 pushes each sub-stage after its gate: the controller performs this task itself (a push is never delegated), and never with force. Its steps stay unticked in the plan, because they run after the close-out commit.
- [ ] Step 1: Check the state to push
git status --short
git fetch origin
git rev-parse main v0.8.3^{commit}
git merge-base --is-ancestor origin/main main && echo fast-forward
git tag --list "v0.8.*"
git ls-remote --tags origin "v0.8.*"
Expected: no output from git status --short; the two SHAs are equal (the tag points at the tip of main); fast-forward (local main contains origin/main, which was at 2e3d3b1 when this plan was written); the tags v0.8.0 to v0.8.3; ls-remote shows which of them the remote has. If origin/main has commits that local main does not, or the remote already has a v0.8.3 that points at another commit, stop and report: nothing is pushed, and no rebase or force push is attempted. If the remote lacks one of v0.8.0, v0.8.1, or v0.8.2, report it to the person; this task pushes only main and v0.8.3.
- [ ] Step 2: Verify a fresh clone of the tag
git clone --branch v0.8.3 . ../BEMGen-v0.8.3-clone
powershell -NoProfile -ExecutionPolicy Bypass -File ../BEMGen-v0.8.3-clone/scripts/verify.ps1
git -C ../BEMGen-v0.8.3-clone describe --tags
Expected: the clone reports a detached HEAD at v0.8.3; verify builds the clone with 0 Warning(s) and 0 Error(s) and ends with VERIFY PASSED, with 193 core, 224 generators, and 704 integration tests passed on net8.0; describe prints v0.8.3. Then delete the clone (rm -rf ../BEMGen-v0.8.3-clone in Git Bash). If verify fails, stop: nothing is pushed.
- [ ] Step 3: Push (no force)
git push origin main
git push origin v0.8.3
git ls-remote --tags origin "v0.8.3*"
git status -sb
Expected: each push succeeds without --force (main as a fast-forward, the tag as [new tag]); ls-remote lists v0.8.3 with its peeled ^{} line; git status -sb shows ## main...origin/main with nothing ahead or behind. A rejected push is not retried with force: stop and report.
Exit criteria¶
docs/research/precedents/syn-typologies.mdparaphrases the 19 families of the pinned four-source records (v0.3.1-research,7538ed3, with the SHA-256 of the three record files), with lineage, support, and the BEMGen generator and sub-stage of each;hdhm-typologies.mdnames itself the issued baseline.- ADR-014 records the new space types, the fifteen illustrative presets with their reasons, the default preset components, and the department zones of every new family (D-097); ADR-006 specifies all nine new families and records 017 and 016 as implemented with the pipeline consequences of S8.4.
- The research brief, the repository spec, README, AGENTS.md, and the roadmap cover building typologies of every program type (D-094), with S8.4–S8.7 before S6 and S9 after S6 and S8.7 (D-098); every relative link in
docs/,README.md, andAGENTS.mdresolves after Slice A (note 3). - Twelve
SpaceTypevalues are appended afterMixed;ExampleNonResidentialPresetsholds the fifteen presets withSpaceTypes,ValuesOf, andPresetOf, tested byExampleNonResidentialPresetsTests(35 tests), which failed to build on the code before it. - Fifteen default preset components in 1 Program and two generator components in 2 Generate, with the GUIDs of this plan, their defaults, required preset inputs, Preview Location last on the generators, and icons (53 icons, the existing ones byte-identical); no existing component, input, or GUID changed.
OfficePlateGenerator(017, an office ring around a central core without outdoor walls, at least 3 m of work area on every side) andCafeteriaGenerator(016, a kitchen and servery along the whole north edge of a larger dining field) implement ADR-006 with their defaults, zone IDs, and validation; each has tiling, window, invalid-parameter, preset, determinism, provenance, and snapshot tests that failed to build on the code before them.- Both families validate with every plan simplifier and floor aggregator, on their canonical plans and at two rotated and moved placements (
OfficePlateTypologyTests, 51 cases;CafeteriaTypologyTests, 50 cases), with noLod.Corechange. - Snapshots: the sixteen existing snapshots byte-identical; two new canonical snapshots.
pipeline.md,domain-model.md,convert2bem.md, ADR-002, andprogram-presets.mddescribe S8.4; the S8.4 smoke-test checklist and its headless result are in the guide.scripts/verify.ps1printsVERIFY PASSED; tests: 193 core, 224 generators, 704 integration onnet8.0; 0 warnings.- Version 0.8.3, tag
v0.8.3; AGENTS.md, README, the roadmap Progress line and detailed-plans row, and the decision-log entries D-099, D-100, and D-101 are written; this plan is ticked and marked Done. - After a fresh clone of
v0.8.3passesscripts/verify.ps1,mainandv0.8.3are pushed without force (D-098).
Notes for the reviewer¶
- Source. Every code block, snapshot, and documentation replacement of Tasks 1 to 10 was extracted programmatically from a verified reference implementation built on
mainat2e3d3b1, 10 commits fromc3dd00btof841f0c, each passingscripts/verify.ps1with 0 warnings. The reference's commit order is kept with one exception: its ADR-014 commit (e5ba806, the third) is Task 2, before the scope alignment (a0c4f97, the second, Task 3), because AGENTS.md, the repository spec, and the roadmap link to ADR-014; the two commits touch disjoint files, so every file content in this plan is the content of the reference commit it comes from. The only content not taken from the reference is this plan's own commit, the three anchors of Task 3, Step 6 (note 3), and the close-out of Task 12 (adapted from the reference's proposed texts). Every task was then replayed on a worktree ofmainat2e3d3b1(its branch standing in formain; nothing fetched, pulled, tagged, or pushed), taking each file content and each replacement from this plan's text: the red build errors of Tasks 5, 7, and 8 (with line and column numbers and in the compiler's order), the single failing snapshot test of Tasks 7 and 8 and the received snapshots (byte-identical to the expected content), the failures ofmake_icons.pybefore Tasks 6 and 9 and the 51 and 53 hashes after them (every PNG byte-identical to the reference's), the counts after every task, a 0-warning build after every task, every documentation replacement (each "Replace" text occurs exactly once when it is applied), the link checks, the close-out edits, andVERIFY PASSEDon every merge are observed results. The replay's end state equals the reference:git diff --stat ref/s8.4 replay/s8.4limited to the 47 paths the reference changes prints nothing for the state before the close-out, and after it lists onlyAGENTS.md,README.md,docs/development/grasshopper-smoke-test.md, and the roadmap, whose differences are exactly the close-out texts of Task 12;Directory.Build.props(0.8.3), the decision log, the plans, and the three anchors of Task 3 are files the reference does not change. Every observation of a first replay was observed again, unchanged, in a second replay of the final plan from a fresh worktree. - Slices and branches. The roadmap names no slices for S8.4; this plan uses four slices and a close-out, named after D-004:
docs/four-source-scope(Tasks 1–4),feature/non-residential-programs(Tasks 5–6),feature/office-plate-cafeteria(Tasks 7–9),docs/architecture-non-residential(Tasks 10–11), andchore/repo-s8.4-close-out. Merges rebase onto localmain. Unlike S8.1–S8.3, the stage is pushed at its end (D-098): Task 13 is the controller's, after the tag and a fresh clone of it passscripts/verify.ps1.origin/mainwas at2e3d3b1when this plan was written, so the push is a fast-forward. - Links. The reference's scope commit renames the roadmap heading
### S8 — Typologies (S8.1–S8.3, D-083, D-084)to(S8.1–S8.7, D-083, D-084, D-098), which changes its anchor to#s8--typologies-s81s87-d-083-d-084-d-098; the header lines of the S8.1, S8.2, and S8.3 plans link to the old anchor and would no longer resolve (a link check over every Markdown file of the reference's end state found these three and nothing else). Task 3, Step 6 updates the three links; the reference does not, and these are the only changes to earlier plans. Putting ADR-014 (Task 2) before the scope alignment (Task 3) means that every link of Task 3 to ADR-014 resolves when it is committed. The precedent page and ADR-014 link to each other, so no order of two separate commits resolves both: between the commits of Task 1 and Task 2, the precedent page's two links to ADR-014 point at a file that Task 2 creates; the slice is merged only after both, somainnever holds the unresolved link. A relative-link check (file targets and heading anchors, GitHub's slug rule) over every Markdown file of the replay found: no broken link at the plan commit or after any commit except Task 1's, whose two links from the precedent page to ADR-014 resolve with Task 2; after Tasks 2, 3, 4, 10, and 12, none. - The baseline is 603 integration tests.
5038512(afterv0.8.2, onmain) made Perimeter Core reject self-intersecting offset rings and addedPerimeterCoreNeckTests(two tests); D-092 records 601. S8.4 does not touchStrategies.csor that test. - Tests that were not test-first, and why. The integration matrices of Tasks 7 and 8 (101 cases) passed at once in the reference and in the replay: the surface builder, the four simplifiers, the four aggregators, and validation already handle a zone with a hole inside a footprint without one (the office ring) and a plain two-zone rectangle, so no code change was needed and no red phase is possible; each task says to stop if a case fails. In the reference, an uncommitted sweep of 50 rotations from 0° to 357.7° with translations over the default office plate, an office plate at the 3 m minimum, and the default cafeteria ran 2400 simplifier and aggregator combinations without a failure; it was not replayed. Tasks 6 and 9 are Grasshopper code, checked by the build, the embedded icons, and Task 11. The build-error red phases list only declaration-level errors: the compiler stops before binding method bodies.
MinimumOfficeDepth. The 3 m minimum is a public constant ofOfficePlateGenerator, not aToleranceSettingsvalue: it is a rule of the family (a core near a façade is outsideSYN-TYP-017, and 3 m holds one row of desks with its aisle), likeWingCount ≥ 2, and the depth is compared exactly (3 m is valid, as tested). D-087 (the Perimeter Core precondition) still applies downstream and is not imposed by the generator: the default plate passes it with the 4.57 m default depth.- Department zoning and
SYN-TYP-017. The family speaks of several work areas around the core; D-097 makes the contiguous work area one department zone, a ring. ADR-006 records the tension and follows D-097. The cafeteria's servery counter and the customers' route along it are the shared edge betweenKSandDI, not zones (D-097, ADR-014). - Upper bounds (D-100). ADR-006 says that no family has an upper bound; the hub-and-arms families of S8.5 get a maximum (3–4 lobes or branches, 2–4 bedroom wings, 2–3 halls with a support wing), because the shared layout is axis-aligned. ADR-006 calls this a limit of the layout, not of the families, and the close-out records it as the provisional controller decision D-100, to be revisited in S8.5 if a radial layout is needed. It changes nothing in S8.4.
- Floor heights. ADR-006's sentence that the floor height is 3 m for every family stays for the High-Density Housing families; the new families have their own (3.8 m office, 4 m cafeteria, 5 m mall, 6 m foyer and halls, 4.2 m operating suite, 3 m housing), stated in the new section.
- Rule IDs of
001–009. The precedent page and ADR-006 assume that the rule IDs ofSYN-TYP-001to009equal the HDHM ones: the statements correspond one to one, but the records do not state the mapping. - D-numbers. The precedent page (Task 1) states the
SYN-TYP-*ID rule as a provisional controller decision of S8.4 without a number, so that no number is committed before the controller assigns it; the close-out logs it as D-099, with D-100 (note 8) and the completion entry D-101. - Smoke-test values. The values of Task 11 were observed in one scripted headless Rhino 8.25 session (D-056) on the reference's last commit
f841f0c, which reported the informational version0.8.2+f841f0c…; they were not re-observed on the replay. Task 11, Step 1 checks thatsrc/,tests/, andscripts/of the branch equal the reference's, so the record applies; the replay found no difference. The viewport, the toolbar icons, the Inspect texts beyond the footprint line, the zone IDs, the Validate glazing values, and Convert2BEM with Override = False were not observed. - Presets.
Schedule.FromDailyProfilessupports one weekday and one weekend profile, so the retail schedules (their own Saturday and Sunday) are built by a privateWeeklyhelper onSchedule.Create; no public API is added for it. The label and unit mapping ofDefaultPresetComponentalready covers gas equipment (W/m²) and ventilation (1/h), so no mapping is extended (the brief asked to extend it only if needed). - Icons. The 36 existing PNGs are byte-identical after Tasks 6 and 9 (Pillow 12.3.0); the new ones equal the reference's bytes. The contact-sheet descriptions are from the replay's sheets.
- Clean working tree. The
git statusexpectations assume a working tree with no untracked files outside.gitignorebefore Task 1. check_plans.pyfalse positives. The Global Constraints line that states the no-attribution rule is flagged as a possible attribution; it states the rule. Themake_icons.pydocstring line that names the Pillow version of the icons is flagged, as in S4.1 to S8.3.