← [ back to papers ]
2026-08-24 · WORKING PAPER · DRAFT

StairFit: A Revit Stair Solver That Measures the Room First

▶ watch the demonstration

1. Abstract

Revit shows one stair at a time. StairFit measures the room around the stair with rays, generates every stair that could stand there, and filters them against the stair type, the code, and the measured space. 2,706 candidates. With the Part 3 rule set enabled, 231 survive. 0.43 ms, and the same answer every time. In the recorded demo, code enforcement is intentionally off; Section 6 says why. The AI never computes a number; it picks tools and reads results. The three failures that passed every test are in Section 5.

2. The Problem

The working model holds a 4,000 mm storey and a stair of 22 risers at 166.7 mm:

n0 r0= 22 × 166.7 ≈ 3,667, Δ = H − n0 r0 ≈ 333
  • ≈ because 166.7 is the rounded display of the stair's actual riser height; the solver itself never rounds.

333 mm short. Revit reports that the riser count differs from the desired count, and stops.

Two more facts appear in no warning. The stair at one point aimed at 4,800 mm, a height that is not a floor; the warning tests n0r0 against the aimed height, and nothing tests the aimed height against H. And a stair that overshoots keeps climbing through the slab above, which a section shows instantly and no report mentions.

Fixing this by hand is guesswork. Add risers and the run hits the wall ahead; shorten the run and the risers break the limit; nothing shows where between those two failures the answers live.

Level 0Level 1 · +4,000aimed +4,800 (not a floor)333 short22 risers × 166.7 = 3,667 of 4,000
Figure 1. The stair as drawn: 333 mm short of a 4,000 mm storey, and once aimed at a height that is not a floor. Neither fact appears in a warning.

3. How It Works

Three layers, and the boundary between them is the design. The language model picks the tool and speaks fixed lines; it computes nothing, because a stair is exact: a riser sum that misses the storey height is not almost right, it is wrong. A script makes one Revit round trip. A pure function decides what fits.

language modelone Revit round tripsolve()routes and narratescomputes nothingtype limits + 20 rays3.5 s for two stairs2,706 candidates tested0.43 ms, deterministic
Figure 2. The division of labor. Probability at the edges, determinism at the core.

The rays answer one question: how much room the building actually leaves around this stair, to the nearest slab, wall, and ceiling. The round trip reads the stair type's own limits and casts ten per stair: six up from the nosings for headroom, two sideways for clear width, two along the plan for clearance ahead and behind. A ray that hits nothing returns null: the limit it would have set is dropped, and the report says nothing found, never measured clear. Measuring ahead changed the fit rule: a candidate is tested against the drawn run plus the measured clearance, almost twice what is drawn here.

headroom ×6 · min 2,353ahead 4,750behind 815a ray that hits nothing = "nothing found", not infinite room
Figure 3. Ten rays per stair; the two width rays lie out of this section. The clearance ahead nearly doubles the room the solver may use.

Generate

Every candidate is exact by construction. A stair of n risers over a storey rise H has riser height

r = H / n, so that n · r = H
  • H: the true storey rise, measured at 4,000 mm. Never the aimed height of Figure 1.
  • n: riser count; r: riser height, mm.

solve()deliberately searches wider than the legal band, so a stair that fails stays visible with its reason instead of vanishing. The sweep: 22 riser counts, 41 tread depths in 5 mm steps, 3 flight arrangements,

|C| = 22 × 41 × 3 = 2,706

For the Part 3 clause set used to test the solver, no flight may rise over 3,700 mm. Risers are whole steps, so the flights a candidate needs follow from its own riser height:

kmax = ⌊3,700 / r⌋, Fmin = ⌈n / kmax
for n = 24: kmax= ⌊3,700 / 166.7⌋ = 22, Fmin = ⌈24 / 22⌉ = 2

Every legal answer for this 4,000 mm storey therefore has two flights, and the landing between them takes its length from the stair's required width:

L = min(Wreq, 1,100) = 1,000
  • W(req): the required width of the stair, per OBC 3.4.6.4. The demo stair's 1,000 mm width is taken as its required width.

Plan length follows from one fact: a flight of k risers carries k − 1 treads. So F flights holding n risers between them carry n − F treads and F − 1 landings:

P = (n − F) t + (F − 1) L
for n = 24, t = 280, F = 2: P = 22 × 280 + 1,000 = 7,160

Test

A candidate passes when it violates nothing, and every clause value it is tested against is carried with its number and page:

ConstraintSourceLimit
Riser heightOBC 3.4.6.8, Vol 1 p. 357125 to 180 mm
Tread runOBC 3.4.6.8, Vol 1 p. 357280 to 355 mm
Rise per flightOBC 3.4.6.3, Vol 1 p. 3543,700 mm max
Landing lengthOBC 3.4.6.4, Vol 1 p. 355lesser of required width or 1,100 mm
HeadroomOBC 3.4.3.4(2), Vol 1 p. 3502,050 mm; measured 2,353 passes
Riser, tread, widththe Revit stair type≤170, ≥250, ≥1,000 mm
Plan lengthmeasured≤ drawn run + clearance ahead

A candidate survives only if every check passes: riser and tread inside both the clause band and the stair type's own limits, no single flight rising over 3,700 mm, headroom clear, and the plan inside the measured space. As one line:

pass = type limitscode limits measured space
G = R + Cahead = 5,250 + 4,750 = 10,000
  • R: the drawn run; C(ahead): the measured clearance. If the ray found nothing, no limit is set and the report says so.

Headroom is a three-way verdict, because an unmeasured stair must not pass by default:

Headroom measuredResult
≥ 2,050 mmpass
< 2,050 mmfail
not measuredno verdict, never a pass

The rise limits alone pin where the survivors can sit: r ≤ 170 forces n ≥ ⌈4,000/170⌉ = 24, and r ≥ 125 forces n ≤ ⌊4,000/125⌋ = 32. The 231 survivors span exactly that range.

Code enforcement is a switch, off in the current demo (Section 6 says why). Off still enforces the type limits and the measured space, and 613 survive; what changes is speech. With the code off, the report is forbidden to mention the code, because presenting a buildable stair as a compliant one is the failure this project exists to avoid.

enumeratedpass, code onpass, code off2,706231613
Figure 4. The solution space of the working stair. The code removes every single-flight answer; the type and the measured space remove the rest.

Rank

Ranking among the survivors is plain preference: first a stair that fits inside the run as drawn, then fewer flights, then better proportions by the Blondel step rule, 2r + t near 625 mm. That rule is three centuries old and it is not a clause, so it orders candidates and never rejects one. The first pick is 24 risers on 280 mm treads in two flights: 7,160 mm of plan, 1,910 mm longer than drawn, well inside the measured clearance.

4. The Screen

In this prototype, riser count is the one axis exposed to the designer; tread and flights follow from it. So the screen is a ladder of riser counts. Clicking a cell redraws that stair as a section at measured scale, with the current model overlaid as a gray dashed profile. Two boundaries keep it honest: the preview draws arithmetic, not clash detection, and the dashed profile is the only measured thing on screen; and clicking changes the screen only, since building goes through a spoken confirmation, not yet wired to the clicked cell (Section 6).

← STEEPERGENTLER →the one thing you choose24166.7 riser7,160 planPICK26153.8 riser7,720 plan28142.9 riser8,280 plan30133.3 riser8,840 plan32125.0 riser9,400 plantwo flights · 280 mm treads · 1,000 mm landing · code on
Figure 5. The ladder, drawn with the code-enforced answers: even riser counts from the 231 survivors. Plan length is (n − 2) × 280 + 1,000.

In use it is a conversation, and the spoken yes is the only step that changes the model:

"This stair doesn't fit."   → 333 short, aimed past the floor. Then: 231 ways it can.
"Can we do 26 risers?"      → 153.8 riser · 7,720 plan · fits with 2,280 to spare
"Yes."                      → stair, slab opening, and railing rebuilt

5. What the Math Missed

Three failures during development passed every test and were caught by a person. They stay on the record because each marks a place where arithmetic and architecture diverge.

The landing. The first solver computed run length as (n − 1) × tread: a single flight, always. For a 4,000 mm storey it proposed 24 risers straight up, which OBC 3.4.6.3 forbids: no flight may rise more than 3,700 mm. The tests were green, because they tested the arithmetic and the arithmetic was right.

one flight · rises 4,000forbidden · 3.4.6.3 caps a flight at 3,700two flights · landing 1,000run = (n − F)×t + (F − 1)×landing
Figure 6. The fix that rewrote the geometry. The old formula (n − 1) × t knew no landings; the tests now rederive the flight rule from the clause instead of trusting the solver.

The API that was there. After a failed import from DB.Architecture, the conclusion "this environment cannot draw landings" was built into the tool, the screen, and the tests. StairsEditScope lives one namespace up, at Autodesk.Revit.DB. A failed import means the name is wrong, not that the capability is absent.

The stair is not the whole edit. The first rebuild rebuilt only the stair, which then climbed through an uncut slab with no railing. Opening and railing now rebuild with every stair, since a new run length moves where the slab is cut.

The 7,064 assertions that exist now re-test every passing candidate against every clause independently, check that every proposal is itself legal, and assert determinism by running the solver twice and comparing output byte for byte. They keep these three failures from returning; they did not catch them.

6. Limits

The clauses are Part 3, and the demo building is a house. Part 3's stair scope excludes stairs inside dwelling units; those are governed by Part 9, whose numbers are deliberately not loaded because retrieval could not fetch them cleanly, and a wrong limit enforced confidently is worse than an absent one. This mismatch is why the code switch is off in the demo.

Straight flights only. Winders, L-turns, and U-returns are not modeled. The solver does not measure what a placed landing might hit, and does not subtract the width a railing occupies.

One stair, one model, one machine.2,706, 231, 3.5 s, and 0.43 ms are measurements, not benchmarks. Ten rays are ten samples; geometry between them is unmeasured. The clicked cell is not yet wired to the spoken confirmation.

The pattern is the finding: measure first, enumerate everything, keep the AI away from the numbers. The same split runs through a companion project that compiles a zoning by-law into a clause graph and refuses to issue verdicts.

standing note

No code determination. Nothing on this page is a building-code review, and the clause set carried by the demo does not govern the demo building's own stairs. Any use in practice requires review by a qualified professional against the applicable code.

References

  1. Ontario. Building Code, O. Reg. 332/12: 2024 Building Code Compendium, Volume 1. Sentences 3.4.6.8 (rise and run), 3.4.6.3 (vertical rise of flights), 3.4.6.4 (landing dimensions), 3.4.3.4 (headroom), pp. 350–357.
  2. Autodesk. Revit API documentation: ReferenceIntersector, StairsEditScope. help.autodesk.com
  3. Blondel, F. Cours d'architecture. Paris, 1675–1683. Origin of the 2r + t step rule quoted in Section 3; used here as a ranking heuristic, never a clause.
  4. Cobbe, K., et al. "Training Verifiers to Solve Math Word Problems." 2021. arXiv:2110.14168. On the arithmetic error rates of language models, the reason the model here computes nothing.
  5. Dziri, N., et al. "Faith and Fate: Limits of Transformers on Compositionality." NeurIPS, 2023. arXiv:2305.18654.
  6. Cho, D. "Machine-Readable Zoning: Toronto By-law 569-2013 as a Clause Graph." 2026. The companion project of Section 6.
  7. Demonstration recording of the stair workflow in Revit, 2026. instagram.com/p/Dcauq6IR9B9