Files
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

25 KiB
Raw Permalink Blame History

panellift — plan

PCI Chapter 8 handling checks for precast wall panels, fed from the Revit model that already holds the geometry.

Scope is the "first tier" agreed on 2026-08-18: stripping and yard handling, flat. Transportation (needs dunnage positions the model does not have) and erection (the panel rotating up, load shedding onto the lowest pick point) are explicitly out.


Why the calculation does not run inside Revit

The extension runs on IronPython 2.7 on Revit's UI thread. A calculation whose output goes to a plant has to be verifiable, and verifiable means a test suite that runs in a second without a Revit licence, a model, or a Windows box.

So: Revit writes a JSON file, a plain CPython package reads it. Same pattern already proven by vibe.aiextract. The package has no dependencies — every dependency is something that can be missing at the moment somebody needs a number.

Revit  ──liftexport──>  <mark>.json  ──panellift──>  report.html
(IronPython 2.7)                        (CPython 3.9+)

A consequence worth keeping: a panel can be checked by hand-writing the JSON, with no model at all.

Decisions taken

Question Decision
Lifter capacity vs handling multiplier Not stacked. PCI Table 8.3.1 note (a): those factors are for flexural design and are not to be applied to lifting-device safety factors. Inserts are checked against unfactored reactions and against the published rating of the part itself — safety factors are per product in knowledge/catalogue.yaml and an insert with none published is reported unjudged. This row used to end "with the manufacturer's 4:1 tension / 2.67:1 shear". No manufacturer published that. The sentence was written the day after the constant it justified and it cited nothing; the one datasheet in this repository for the parts it reached (ALP catalog p53, 02/2024) heads its capacity table "Capacity in Concrete 4:1 SWL" across both load columns — 4:1 in shear as well. Corrected 2026-08-31, and recorded rather than quietly deleted, because an invented pedigree on a safety factor is the kind of thing that gets re-derived from the same document twice.
Sling angle Engineer's config file, in degrees. Not inferred from the model.
Rolling block vs static lift Engineer's config file. Two different mechanics, not a coefficient — see below.
Load cases Stripping + yard handling.
Flexural capacity Shipped 2026-08-31 as ACI 318-19 approximate fps (Eq. 20.3.2.3.1), strand only, in capacity.py. The columns were informational for one day. Owner's ruling 2026-09-01 — "M/pMn 对我而言>1.0是可以的,我能盖章。你不行,大于1.0你就报fail,我做最后决策。" — so M/φMn > 1.0 is a FLEX-CAP FAIL and V/φVn > 1.0 a SHEAR-CAP FAIL, checked at every station of every stage, with no development-length exclusion: the tool may not bless a ratio over 1.0 and the engineer stamps past it. The stress check keeps its own verdict; a station that cannot be judged stays a dash and is neither. Full strain compatibility remains the upgrade: it needs the strand stress-strain curve and mild-steel/WWF input paths that no layer holds yet. fy defaults to 60 ksi, fyt from the design summary; both overridable in config.

Rolling block vs static lift

  • rolling — equalising sheaves, every line in a cable group carries the same force. Two groups, two equations of statics, determinate. Reactions do not depend on stiffness, so a blockout does not move them.
  • static — fixed-length slings. The panel is a continuous beam on rigid supports; three or more support lines are indeterminate and the reactions follow flexural stiffness. A blockout beside a support really does shed load off it.

Measured on the ASPIRE panel with three support lines at 4 / 16 / 28 ft:

rolling   15,167  15,167  15,167 lb
static    13,508  18,484  13,508 lb      middle line +22%

Picking the wrong one is a 22% error on an insert. Hence: the engineer says.

The design decision that matters most

PCI Figure 8.3.2's coefficients are a test, not the engine.

Those coefficients only hold when the picks sit where positive and negative moment come out equal. The entire reason to check a real ticket is that the detailer may not have put them there — so using the closed form as the engine would assume the answer.

Instead the moment comes from a real beam solve at the actual insert positions, and Figure 8.3.2 becomes the regression test: put the supports at (sqrt(2)-1)/2 of the span and the solver must return 0.0214 w a b².

It does. That identity is also derived in pci.py rather than quoted — solving w(aL)²/2 = wL²(1-2a)²/8 - w(aL)²/2 gives 4a² + 4a - 1 = 0, hence a = 0.20711 and a coefficient of 0.02145, which is PCI's 0.0214.

Known idealisation

The panel is treated as a beam spanning its long direction. This is PCI's own method for My, and it is what the ASPIRE examples do. It is not valid for a near-square panel, where two-way plate action governs — that is finite-element territory (PanelsPlus). The code must refuse rather than answer when the aspect ratio gets close to 1. Not yet implemented.

The transverse direction Mx will use PCI's closed form with the "resisted by 15t or b/2" rule, as a secondary check.

Status

Done and tested — 34 tests, all passing

Module
model.py Panel, Blockout, Lifter, Rigging, Reinforcement, Stage, Material
section.py b_eff(x), S(x), I(x), w(x), blockout merging, weight reconciliation
pci.py Table 8.3.1, 5√f'c, Figure 8.3.2 closed forms, optimal support fraction
linalg.py dense solver, so there are no dependencies
statics.py insert clustering → support lines → cable groups; rolling and static solvers; V(x), M(x); sling forces

Reproduced from ASPIRE Winter 2019 (Brown), which reprints PCI Table 8.3.1 and Figure 8.3.2:

ours published
unit weight 109.4 psf 109.4
S solid 1531 in³ 1531
S, five 6"×24" pockets 1148 in³ 1148
unfactored moment 24,020 lb-ft 23,973
stripping stress, solid 245 psi 244 — OK vs 316
transport stress, solid 282 psi 282 — OK vs 354
stripping stress, pocketed 326 psi 326 — no good vs 316
transport stress, pocketed 377 psi 376 — no good vs 354

The moment differs by 0.2% because the article rounds the coefficient to 0.0214; we carry (√2−1)²/8.

Done since — 76 tests

checks.py cracking, insert capacity with sling amplification, edge-anchor shear and plumb-hang tension, per-stage derating, aspect-ratio refusal, unrated inserts as a warning
report.py self-contained HTML; build_many puts every stage of one piece in one file behind a verdict table
layout.py recommended insert positions and the delta from the model
cli.py panellift job.json -o report.html [--json]
vibe/liftexport.py the Revit side of the job file, plus the subprocess
Panel Lift Check the button: select an assembly, confirm the picks, choose where the HTML goes

Next

  1. Strain-compatibility φMn (upgrade of capacity.py's approximate-fps columns, shipped 2026-08-31): strand power curve, mild steel and WWF layers — the input paths for both are still owed before this can be honest.
  2. Transverse equilibrium. layout.py reports how far a row's centroid sits off the width centreline, but a beam model cannot say what that costs. On a panel with large openings the recommended rows can be forced entirely to one side, and whether the piece twists is currently the engineer's call with no number attached.
  3. Per-piece config overrides (sling angle, lift mode) without editing the plant knowledge file.

Answered against the live model (sample_job, 2026-08-18)

Probed on PNL204 — a PANEL_VERTICAL::08", design PNL1.0, 13'-10" x 4'-0" x 8", 5,533 lb, 51 members.

Panel-local frame. x is the width, y is the thickness, z is the length. On PNL204: x ∈ [-2.000, 2.000], y ∈ [-0.667, 0.000], z ∈ [0, 13.917] ft. So the form face is y = -0.667 and the exposed face y = 0.

1. Blockouts are family instances, not Opening elements. The document contains zero Opening instances. Voids are a family group under Generic Models:

family count
VOID_RECTANGULAR 622
VOID_RECESS_3_SIDES 234
VOID_POUR_STRIP 102
VOID_DT_DAP 54
VOID_ROUND 46
V-PLATE_VOID_RECTANGULAR 22
VOID_TAPER_ROUND 17

STEM_BLOCKOUT (2694) is under Specialty Equipment and belongs to double-tee stems, not wall panels. So b_eff(x) gets populated the same way the lifters do: instance position transformed to panel-local, size from instance parameters. Whether these families actually cut the solid, or only mark the void for the detailer, still needs checking — it decides whether the geometric weight reconciliation will agree.

2. Insert role separates cleanly by geometry. On PNL204:

family count x y z
ANCHOR_FACE_4T 4 ±1.000 -0.667 (form face) 2.583, 11.417
LIFT_EDGE_4T 2 ±1.000 -0.333 (mid-thickness) 13.917 (top end)

Face-mounted and inboard → stripping. Mid-thickness and at an end → erection, exactly as suspected of the LIFT_EDGE_8T seen at 0'-0". Two independent signals agree, family prefix and position, so the rule can check itself rather than trusting a name list.

The real pick is 4 points on 2 support lines at z/L = 0.186 and 0.180, against the PCI optimum of 0.207 — a good first live test case.

Capacity is encoded in the family name: ANCHOR_FACE_4T → 4T, LIFT_EDGE_4T → 4T, LIFT_EDGE_8T → 8T. That makes knowledge.yaml's capacity_lb table largely derivable. Open question for the engineer: is 4T the working load limit or the ultimate? It is a factor of four on every insert check.

3. Strand positions are recoverable, through-thickness included. Four 1-2_STRAND at y = -0.500 and y = -0.167 — two layers, 2 in. from each face of the 8 in. panel. That y is precisely the depth strain compatibility needs. WWF_MESH_A sits in the same two layers, confirming per_face in the spec schema.

Unresolved: the four strands sit at only two x stations (-1.500 and 0.000) on a panel spanning -2.000 to 2.000, which is not symmetric. Either the family insertion point is off-centre or _point_of is returning a curve start. Worth pinning before trusting a lever arm.

4. Panels are boxes, and there are only two thicknesses. PNL204's local bounding box is a clean 4.000 x 0.667 x 13.917. The whole model uses only PANEL_VERTICAL and PANEL_HORIZONTAL, each in 08" and 12".

One discrepancy to carry: the bounding box gives 13.917 ft = 13'-11" while the panel's stated length parameter says 13'-10". Use the stated dimensions for the calculation and report the delta, the same way a ticket's printed length runs over the modelled one on stemmed pieces.

Smaller known gaps

  • Three support lines with equal gaps on both sides make the cable grouping ambiguous; currently resolved to the first. Should warn.
  • Partial-depth blockouts are carried but do not reduce S.
  • Inclined slings: the horizontal component is reported, but the secondary moment it induces is not analysed. LECWall does not either, and says so.

Insulated (sandwich) panels

Added 2026-08-31. Read this before touching sandwich.py or the refusal in cli._refuse_sandwich.

Why it is a refusal first and mechanics second

Every section property in this package is built on one prism of concrete panel.thickness_in deep — section.NetSection, section_modulus_in3, the four-corner stress. An insulated body is two concrete wythes with insulation between them, and idealising it as solid concrete at the envelope depth overstates the section modulus and understates the tensile stress. The owner, 2026-08-31:

"把夹芯板当实心算并不 conservative。因为 insulation 并不会 100% composite, 你忽视了中间的 composite ratio 的计算和可能出现的问题。这是一个 insulated panel design 的时候非常大的一个问题。"

On the dimensions of a real 16 in. piece (4 in. architectural wythe, 4 in. EPS, 8 in. structural wythe, 64 in. of clear width) the arithmetic is:

I, in⁴ governing S, in³
solid at the 16 in. envelope 21,845 2,731
fully composite 20,139 2,324
non-composite 3,072 768
structural wythe alone 2,731 683

So the solid idealisation overstates the governing section modulus by 3.56× against the non-composite bound and still 1.18× against the fully composite one. Both above 1.0: there is no reading of a sandwich under which treating it as solid is the safe direction, because a sandwich has a hole in the middle of its depth and is weaker than a solid piece of the same envelope however well the connectors work. test_sandwich.py keeps every one of those numbers with the arithmetic in a comment.

Until 2026-08-31 this never fired. The weight gate refused every insulated piece for an unrelated reason — the solid sum was one wythe — and that refusal accidentally protected against this. The weight is right now, so the gate opens, and it opens onto a wrong section. cli._refuse_sandwich is what closes that window, and it is deliberately upstream-independent: it does not care whether the weight gate let the piece through. If a sandwich job file reaches this package at all, it fires.

The detection signal travels in the job file

source.solid_parts — the per-solid breakdown the Revit side has written since 2026-08-31. Nothing on this side re-derives it: the file is the boundary, and a second opinion about a model this process cannot see would be a guess with a different accent. The ladder is in sandwich.detect and the only rung that may return False is one concrete solid and nothing left out. No signal at all returns None, never False.

The conventions chosen, and why each is the conservative one

Every one of these is stated on the number it touches, in the report and in sandwich.py, and every one is on the owner-question list below.

decision what was chosen why it is the conservative reading
wythe load sharing, non-composite bound the wythes are tied by the connectors so they deflect together; each takes the moment in proportion to its own stiffness, M_i = M·I_i/ΣI, so f = M·(t_max/2)/ΣI it is the compatible answer, and the extreme fibre lands on the thickest wythe rather than being averaged over both. The alternative — sharing by mass — puts less moment on the stiff wythe than compatibility does
is the face wythe structural reported both ways. The non-composite bound assumes both wythes resist; structural_wythe_alone assumes the thickest carries the whole piece and the thinner is dead load the second is the smallest section modulus of the three (683 in³ against 768), so printing it is the conservative half and printing only it would hide the bound the owner may actually want
which wythe is structural the thickest, by trade convention not measured, and it is named as not measured every time it is used. There is no conservative direction available here, only a disclosed one
openings assumed to run through both wythes, so b_eff(x) is shared the measured plan areas of the two wythes on the real piece differ by ~1%, which is inside the rounding on the volumes. If they diverge on some piece, each wythe needs its own opening set
insulation contributes nothing to the section its modulus is three orders below concrete's. This is the standard treatment, not an approximation invented here
modular ratio 1, no transformation both wythes are the same mix on every piece measured. Where the densities differ the report says so rather than quietly scaling one
connector demand distribution the mean over a row, at the fully-composite VQ/I, named as a mean and not as a bound it is honest about being an assumption. It is not conservative — see below — and saying so is the only available treatment until k exists

The bounds are scaffolding, not the product

The owner's instruction, and it reshapes what this is for. The two-bound envelope is what can be computed while the connector data is absent. The partial-composite middle is the thing insulated-panel design is actually about, and the capacities and stiffnesses being hunted are the inputs to computing it, not garnish. The report presents the bounds as an interim envelope with the missing computation named.

可能出现的问题 — what the two bounds do NOT bracket

sandwich.concerns() is the enumeration, and reading it is the point. Three quantities are bracketed. Everything else about a sandwich panel is not a bounds question at all, and a reader who takes a two-bound report as an envelope over all of it is the failure the list exists to prevent.

Bracketed:

  • wythe flexural tension — maximal non-composite, monotone in between. With one trap: the inner face of a wythe changes sign between the bounds (compression non-composite, tension fully composite), so both faces of both wythes have to be evaluated, not only the envelope faces.
  • interface shear flow — maximal fully composite, q = VQ/I, zero non-composite. A true upper bound on the total interface force, which is why it is computed even where the plant designs the panel non-composite.
  • deflection — maximal non-composite. Differential bowing from temperature and shrinkage is a service problem, not a handling one, and is declared out of scope rather than silently omitted.

Not bracketed:

  • connector force distribution along the span. Both bounds are distribution-free — one puts zero on every connector, the other spreads demand smoothly as V(x). Real partial interaction concentrates slip, and therefore connector force, at the ends of each shear span, decaying over a length set by the connector stiffness k against the wythes' axial stiffness EA. A uniform-demand assumption understates the end connectors. When k arrives, the standard partial-interaction treatment is what goes in here; until then the mean is reported and named as a mean.
  • connector force at discontinuities — an opening edge or a pick point restarts the slip profile and the connectors beside it see a local peak neither bound shows.
  • connector NORMAL (tension) demand. A flat pick hangs one wythe off the other through the connectors in direct tension; an erection pick peels the face wythe as the piece rotates. Not an interface-shear question at all, so neither bound speaks to it — and where the panel is designed non-composite it is likely the governing connector demand. This is why a capacity is needed per failure mode, not as one number.
  • local flexure of the face wythe between connectors — a slab strip on discrete supports at the connector spacing. A different mechanism from anything in this package.
  • percent composite is not one number. The stiffness ratio (κ_I = (I_eff − I_nc)/(I_full − I_nc), what governs deflection and bowing) and the strength ratio (η, the degree of shear connection, what governs capacity) are different quantities in the trade's own methods. Any percentage this report prints says which one it is.

What stands down, and what unblocks it

stood down why unblocked by
composite ratio (both of them) a computed ratio without connector data is not hard, it is impossible. The width of the band is printed; the position in it is not connector slip stiffness k, lb/in per connector (stiffness ratio); connector capacity per failure mode (strength ratio)
connector sufficiency no capacity is published in any layer. verdict stays None — never OK, never a pass capacity per failure mode: interface shear and normal/tension, lb per connector
connector recognition on the gather side nothing reads the connector instances off the piece. source.connectors is written with every field null and the reason beside it, because a missing key reads downstream as "this piece has no connectors" a recognition rule. Measured by the bridge holder, and it is plant knowledge so the parameter names live in the plant layer: identity is a type-level parameter, not the family name, and a second type-level parameter is the gate. Family-name matching silently finds nothing here — a decoy family with no instances carries the same identity string
the layup on an old job file solid_parts without z0_in/z1_in says a sandwich exists and cannot say what its section is. Layup.measured is False and section_bounds refuses rather than inventing a layup re-export the piece

The plant calls its pins non-composite, and that settles nothing

The insulation family on these pieces carries the description "EPS with Holes for Non-composite pins". That is evidence about design intent, carried verbatim in source.connectors.declared_action and quoted in the report. It is not a structural ruling, and nothing in the mechanics branches on it: partial composite action exists physically whatever the label says, and the connector product has published stiffness data. What the owner's confirmation would settle is which bound the pass/fail line is drawn against — the report carries both either way.

Which end is up, and why a disclosed fallback is allowed

knowledge.up_end_from_origin is this project's flagship declared-coverage refusal — three measured conditions, and it never returns False. When it declines, liftexport.erected_orientation reads global elevation and answers anyway, writing the refusal's own reason into source.up_end_basis_why — and nothing read that key, partly because read_panel looked for source inside panel while the exporter writes it at job level, so panel.source came out empty on every real job file.

Both halves are fixed. The job-level source is merged onto the panel, and report.up_end_disclosure renders the fallback on the page, in the CLI's stdout and in the summary's basis key — written only when the ruled mechanism did not answer, the same "only when it is not the ordinary case" rule searched and enabled follow.

The deliberate decision, because "never fall back" is not the answer either:

  • The rule declines on a vertical wall panel because it is only confirmed for horizontal ones. Global elevation is the mechanism that is sound for a piece erected on end — its two ends really are at different heights — so on that decline the fallback is the right tool for the case the rule handed over, not a guess.
  • It is meaningless for a piece erected long-ways, and that case never reaches here: MIN_VERTICAL_SHARE refuses any piece with no axis convincingly up, and the aspect-ratio gate refuses a near-square one.
  • It reaches one stage. Stripping and transportation are analysed flat and are invariant under mirroring x — reverse the piece end for end and the beam problem is its own mirror image. Only erection depends on it: which row exclude_top_rows drops, and which edge the anchors are on.

So the fallback answers, and it is stated on every report it touched, naming the rule's own reason for standing down. default_stages gets the same treatment one step further on: an absent source.erected_vertical_axis used to be read as or 'length', and now says so in problems.

Owner questions

Nothing below is inferred and nothing below is guessed at. Numbered so they can be answered out of order.

  1. Are the insulated panels designed, and to be checked, as non-composite? The plant's own family description says the pins are non-composite. Confirming it picks which bound the pass/fail line is drawn against; the report carries both either way.
  2. Which wythe is structural? The thickest is assumed, by trade convention, and named as an assumption every time. A ruling makes it a fact.
  3. Is the architectural wythe to be relied on in flexure at all, or carried by the connectors and resisting nothing? The two readings differ by 768 in³ against 683 in³ on the reference piece.
  4. What depth should the beam use once the refusal is lifted — the envelope, one wythe, or a bounded pair? This is the question docs/lift-check.md has been carrying as open since 2026-08-31, and the bounds are now computed for all three so it can be answered against numbers.
  5. Connector capacity per failure mode — interface shear, and normal/tension — in lb per connector, and the concrete strength the ratings were derived at.
  6. Connector slip stiffness k, lb/in per connector. Without it there is no ratio and no end-concentration factor, and both stay stood down.
  7. Do the openings run through both wythes on every piece? Assumed yes; the measured plan areas on the reference piece agree to ~1%, which is inside the rounding.
  8. Is the local thickness axis' minimum face the form face? The layup is measured from the lowest face of any solid the body draws, which is the same convention the strand and insert z_in already use, and which face that is has never been established. A symmetric layup is the same either way; an asymmetric one may be the mirror of what was cast.